XFE 技术 生活 笔记 文集

zepto源码分析·core模块

🔖 javascript 👀 218 🕒 2018-07-12 👨‍🎓 戡玉

准备说明

  • 该模块定义了库的原型链结构,生成了Zepto变量,并将其以'Zepto'和'$'的名字注册到了window,然后开始了其它模块的拓展实现。
  • 模块内部除了对选择器和zepto对象的实现,就是一些工具方法和原型方法的定义。
  • 值得一提的是,内部很多实现都利用了原生数组的方法,很多api也是基于内部或公开的方法进一步拓展实现的。
  • 虽然该模块涉及的api非常多,但内部实现上比较统一,因此只会针对性地挑一些方法进行分析。

实现内容

var Zepto = (function () {
    // 1.基础变量定义
    // 2.内部方法实现
    // 3.zepto对象实现——$('选择器')
    // 4.$.extend方法实现
    // 5.zepto.qsa内部方法和其它5个内部方法
    // 6.全局方法实现——$.方法
    // 7.原型方法实现——$().方法
    // 8.原型链结构设置
}();
  • 上面是主体实现内容,代码定义顺序大致如上,某些地方会穿插定义

重点分析

选择器

选择器的实现逻辑放在了zepto.init方法中,由$()内部调用zepto.init方法,先处理选择器生成dom集合,然后将集合传入能生成zepto对象的内部方法,主体逻辑如下:

zepto.init = function(selector, context) {
    // 1.如果selector为空
    // 2.如果selector为字符串
         a.标签字符串形式
         b.context存在的处理
         c.css选择器
    // 3.如果selector为方法
    // 4.如果selector为zepto对象
    // 5.如果selector为其它情况
    // 6.执行zepto对象生成方法
}

源码如下

zepto.init = function(selector, context) {
    var dom
    // 如果selector不存在(返回一个空的zepto对象)
    if (!selector) return zepto.Z()
    // 如果selector为字符串
    else if (typeof selector == 'string') {
        selector = selector.trim()
        // 如果是标签字符串形式(在Chrome 21 and Firefox 15中,如果选择器不以'<'开头,会引发dom错误)
        if (selector[0] == '<' && fragmentRE.test(selector))
            dom = zepto.fragment(selector, RegExp.$1, context), selector = null
        // 如果存在context属性,则应该以它为基础往下查找
        else if (context !== undefined) return $(context).find(selector)
        // 如果是css选择器
        else dom = zepto.qsa(document, selector)
    }
    // 如果selector是方法
    else if (isFunction(selector)) return $(document).ready(selector)
    // 如果是zepto对象(直接返回)
    else if (zepto.isZ(selector)) return selector
    else {
        // 如果是本身数组,则去掉不存在的
        if (isArray(selector)) dom = compact(selector)
        // 如果是对象,则存为数组形式
        else if (isObject(selector))
            dom = [selector], selector = null
        // 这里重复了typeof selector == 'string'时的判断
        // 因为typeof无法让new String()包装类型进入条件,因此通过最后的else再进行一次判断
        else if (fragmentRE.test(selector))
            dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
        else if (context !== undefined) return $(context).find(selector)
        else dom = zepto.qsa(document, selector)
    }
    // 创建zepto集合
    return zepto.Z(dom, selector)
}

zepto.qsa方法

// 处理css选择器的情况
zepto.qsa = function(element, selector){
    var found,
        maybeID = selector[0] == '#',
        maybeClass = !maybeID && selector[0] == '.',
        nameOnly = maybeID || maybeClass ? selector.slice(1) : selector,
        // 是否为单一形式的选择器而非复杂形式
        isSimple = simpleSelectorRE.test(nameOnly)
    return (element.getElementById && isSimple && maybeID) ? // Safari游览器的DocumentFrament没有getElementById方法
    // id选择器处理
    ( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
    // 非id选择器处理
    (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
    slice.call(
        isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment没有getElementsByClassName/TagName方法
        maybeClass ? element.getElementsByClassName(nameOnly) : // 如果是class
        element.getElementsByTagName(selector) : // 如果是标签
        element.querySelectorAll(selector) // 其它都用querySelectorAll处理
    )
}

zepto.fragment方法

// 处理标签字符串的情况
zepto.fragment = function(html, name, properties) {
    var dom, nodes, container
    // 如果是无标签内容的标签,则直接创建并赋值给dom变量
    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
    // 如果是有内容的标签,则走其他的判断流程
    if (!dom) {
        // 保证标签字符串为双标签形式
        if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
        // 如果没有从html字符串中获取到标签名,则再次获取
        if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
        // 如果标签名为非常规字符串,则name置为'*',容器container设置为div
        if (!(name in containers)) name = '*'
        // 通过容器标签将html字符串dom化
        container = containers[name]
        container.innerHTML = '' + html
        // 返回dom集合,清空container
        dom = $.each(slice.call(container.childNodes), function(){
            container.removeChild(this)
        })
    }
    // 如果是纯粹的object对象
    if (isPlainObject(properties)) {
        nodes = $(dom)
        $.each(properties, function(key, value) {
            // 如果是库的特殊属性名,则应该通过方法调用
            if (methodAttributes.indexOf(key) > -1) nodes[key](value)
            // 如果不是,则设置集合的节点属性
            else nodes.attr(key, value)
        })
    }
    return dom
}

zepto对象

Zepto对象的生成依赖于一连串相关的内部方法,源码实现如下

// zepto对象工厂————第四步
function Z(dom, selector) {
    var i, len = dom ? dom.length : 0
    for (i = 0; i < len; i++) this[i] = dom[i]
    this.length = len
    this.selector = selector || ''
}

// 处理标签字符串
zepto.fragment = function(html, name, properties) {
    // 源码不再赘述...
}

// 生成zepto对象————第三步
zepto.Z = function(dom, selector) {
    // 通过模块的原型链结构,可获得各种操作方法
    return new Z(dom, selector)
}

// 判断是否为zepto对象
zepto.isZ = function(object) {
    return object instanceof zepto.Z
}

// 生成dom集合,然后进行zepto集合创建————第二步
zepto.init = function(selector, context) {
    // 生成dom集合
    // 执行zepto.Z方法
    // 源码不再赘述...
    return zepto.Z(dom, selector)
}

// 获取zepto对象————第一步
$ = function(selector, context){
    return zepto.init(selector, context)
}

// 处理css选择器
zepto.qsa = function(element, selector){
    // 源码不再赘述...
}

// 设置原型方法
$.fn = {

}

// 设置原型链结构
zepto.Z.prototype = Z.prototype = $.fn

// 返回给外部Zepto变量,然后通过window注册
return $

部分api方法

  • 工具方法和原型方法太多,只能挑几个典型了
  • extend和ready的实现都比较简单
  • css方法的实现主要还是对原生element.style和getComputedStyle的熟悉,对属性名转换和样式单位的自动添加的考虑
  • 插入操作的几个方法实现比较精巧,可以认真研究下

$.extend

function extend(target, source, deep) {
    for (key in source)
    // 如果是深拷贝
    // isArray判断是否为数组类型
    // isPlainObject判断是否为纯粹的object类型
    if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
        // 如果拷贝对象的属性值为Object,而目标对象的属性值不为Object,则目标对象的属性值重置为Object
        if (isPlainObject(source[key]) && !isPlainObject(target[key]))
            target[key] = {}
        // 如果拷贝对象的属性值为Array,而目标对象的属性值不为Array,则目标对象的属性值重置为Array
        if (isArray(source[key]) && !isArray(target[key]))
            target[key] = []
        // 进行深拷贝
        extend(target[key], source[key], deep)
    }
    // 如果是浅拷贝
    else if (source[key] !== undefined) target[key] = source[key]
}

$.extend = function(target){
    var deep, args = slice.call(arguments, 1)
    // 如果存在深/浅拷贝设置
    if (typeof target == 'boolean') {
        // 存储深浅拷贝判断值
        deep = target
        // 获得目标对象
        target = args.shift()
    }
    // 遍历拷贝对象
    args.forEach(function(arg){ extend(target, arg, deep) })
    return target
}

$.fn.ready

$.fn = {
    ready: function(callback){
        // 需要检查IE是否存在document.body,因为浏览器在尚未创建body元素时会报告文档就绪
        if (readyRE.test(document.readyState) && document.body) callback($)
        else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false)
        return this
    }
}

$.fn.css

$.fn = {
    css: function(property, value){
        // 如果是获取属性(参数小于2)
        if (arguments.length < 2) {
          // 取第一个元素
          var element = this[0]
          // 如果是字符串类型
          if (typeof property == 'string') {
            if (!element) return
            // element.style用于访问直接样式,getComputedStyle用于访问层叠后的样式
            // camelize方法用于将a-b,转换aB,驼峰转换
            return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)
            // 如果是数组类型
          } else if (isArray(property)) {
            if (!element) return
            var props = {}
            // 存储层叠样式集
            var computedStyle = getComputedStyle(element, '')
            // 遍历数组样式名,生成样式对象props返回
            $.each(property, function(_, prop){
              props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
            })
            return props
          }
        }

        var css = ''
        if (type(property) == 'string') {
          // 如果属性值不存在,遍历删除
          if (!value && value !== 0)
            this.each(function(){ this.style.removeProperty(dasherize(property)) })
          else
            // dasherize用于将property规范化为'a-b'形式
            // maybeAddPx用于自动增加px
            css = dasherize(property) + ":" + maybeAddPx(property, value)
        } else {
          for (key in property)
            // 如果属性值不存在,遍历删除
            if (!property[key] && property[key] !== 0)
              this.each(function(){ this.style.removeProperty(dasherize(key)) })
            else
              css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
        }
        // 遍历设置样式字符串
        return this.each(function(){ this.style.cssText += ';' + css })
      }
}

$.fn.插入

$.fn = {
    // 递归执行节点处理函数
    function traverseNode(node, fun) {
      fun(node)
      for (var i = 0, len = node.childNodes.length; i < len; i++)
        traverseNode(node.childNodes[i], fun)
    }

    // after|before|append|prepend与insertAfter|insertBefore|appendTo|prepend实现
    // adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ]
    adjacencyOperators.forEach(function(operator, operatorIndex) {
      // inside为0和2, after|before(标签外插入)
      // inside为1和3, append|prepend(标签内插入)
      var inside = operatorIndex % 2

      // after|before|append|prepend实现
      $.fn[operator] = function(){
        // 获得dom数组
        var argType, nodes = $.map(arguments, function(arg) {
              var arr = []
              // 判断参数项类型
              argType = type(arg)
              // 如果是array类型,处理成dom数组返回
              if (argType == "array") {
                arg.forEach(function(el) {
                  // 如果el是dom对象
                  if (el.nodeType !== undefined) return arr.push(el)
                  // 如果el是zepto对象
                  else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
                  // 如果el是标签字符串
                  arr = arr.concat(zepto.fragment(el))
                })
                return arr
              }
              // 如果为object类型或不为null,就返回自身;否则作为标签字符串处理,返回dom
              return argType == "object" || arg == null ?
                arg : zepto.fragment(arg)
            }),
            parent, copyByClone = this.length > 1

        // 如果不存在,则返回自身
        if (nodes.length < 1) return this

        // 处理dom数组
        return this.each(function(_, target){
          // inside为0和2, 处理的是after|before, 用父级做容器
          // inside为1和3, 处理的是append|prepend, 用自身做容器
          parent = inside ? target : target.parentNode 
          // target在insertBefore方法中用作参照点
          // operatorIndex == 0, 操作的是after方法, 取下一个节点
          // operatorIndex == 1, 操作的是prepend方法, 取第一个子节点
          // operatorIndex == 2, 操作的是before方法, 取自身
          // operatorIndex == 3, 操作的是append方法, 取null
          target = operatorIndex == 0 ? target.nextSibling :
                   operatorIndex == 1 ? target.firstChild :
                   operatorIndex == 2 ? target :
                   null
          // 是否在html内
          var parentInDocument = $.contains(document.documentElement, parent)
          // 遍历处理插入的节点
          nodes.forEach(function(node){
            // 因为操作对象可能有多个, 而被插入node只有1个
            // 如果要让所有操作对象都被插入内容,需要对插入node进行深克隆
            if (copyByClone) node = node.cloneNode(true)
            // 如果不存在parent,则删除插入的节点
            else if (!parent) return $(node).remove()
            // 插入节点
            parent.insertBefore(node, target)
            // 如果在html内,就递归执行,遇到有js代码的script标签,就执行它
            if (parentInDocument) traverseNode(node, function(el){
              if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
                 (!el.type || el.type === 'text/javascript') && !el.src){
                // 这里用到ownerDocument判断而不直接用window,是考虑到页面有可能是iframe引入的
                var target = el.ownerDocument ? el.ownerDocument.defaultView : window
                target['eval'].call(target, el.innerHTML)
              }
            })
          })
        })
      }

      // insertAfter|insertBefore|appendTo|prepend实现
      $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
        $(html)[operator](this)
        return this
      }
    })
}