从 zepto 源码分析之$构造器的实现

下载源码打开看了一下,几百上千行代码,安慰自己别慌,毕竟透出了这么多方法,这代码量已经很少了。


$ 的实现:

1⃣️:$ 方法

$ = function (selector, context) {
      return zepto.init(selector, context)
    }

2⃣️:init 方法:完成初始化工作

zepto.init = function(selector, context) {
 1  var dom
 2  if (!selector) return zepto.Z() // 传送门1 zepto.Z
 3  else if (typeof selector == 'string') {
 4    selector = selector.trim()
 5    if (selector[0] == '<' && fragmentRE.test(selector))
 6      dom = zepto.fragment(selector, RegExp.$1, context), selector = null
 7    else if (context !== undefined) return $(context).find(selector)
 8    else dom = zepto.qsa(document, selector)
 9  }
 10 else if (isFunction(selector)) return $(document).ready(selector)
 11 else if (zepto.isZ(selector)) return selector
 12 else {
 13   if (isArray(selector)) dom = compact(selector)
 14   else if (isObject(selector))
 15     dom = [selector], selector = null
 16   else if (fragmentRE.test(selector))
 17     dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
 18   else if (context !== undefined) return $(context).find(selector)
 19   else dom = zepto.qsa(document, selector)
 20 }
 21 return zepto.Z(dom, selector)
 }

概括:根据 selector 选择器和 context 属性筛选条件搜索符合条件的 DOM 节点并返回。
详情解读:
第2行:当 selector 未定义的时候,出现了zepto.Z()
往下看......
第5行:出现了 selector ,扒扒文档看看 $ 的用法:

$('div')  //=> 所有页面中的div元素
$('#foo') //=> ID 为 "foo" 的元素
// 创建元素:
$("

Hello

") //=> 新的p元素 // 创建带有属性的元素: $("

", { text:"Hello", id:"greeting", css:{color:'darkblue'} }) //=>

Hello

selector 是元素选择器,也可以看作元素的标志字符串形式。

继续看代码,selector[0] == '<' && fragmentRE.test(selector) ,
Reg的定义为:fragmentRE = /^\s*<(\w+|!)[^>]*>/,意思可以兼容空格类型开头的,右尖括号(>)前不能是以‘>’结束的自闭合/未闭合标签。

selector 是标签的时候就会执行第6行代码

第7行:selector 不是标签且属性筛选条件 context 不为空,先命中 context 再去寻找 dom 节点

第8行:selector 不是标签且属性筛选条件为空,进入 zepto.qsa 方法

第10行:selector 为函数的时候会进入 ready 方法,在DOM树建成的时候触发,是这个库的小彩蛋吧

第11行:selector 是 zepto.Z 的实例直接返回

第13行:selector 是数组类型则表明数组属性是经历过筛选的 DOM 节点,过滤掉为 null 的值

第14~15行:selector 是对象的时候这里会认为是个已经经历过 new Z(dom, selector)的 DOM 节点,用数组包装一下,并释放 selector

第16~17行:selector 是自闭合/未闭合标签,借助 zepto.fragment 转化成真正的 DOM

第18~19行:跟 selector 为 string 类型里的处理是一样的

第21行:调用构造方法,创建 Zepto.Z 对象并初始化对象的属性并返回。

----------------------------------------------❀❀❀❀完结撒花❀❀❀❀----------------------------------------------

zepto.Z() :

zepto.Z = function(dom, selector) {
  return new Z(dom, selector)  // 传送门  Z
}

⚠️:Z():

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 || ''
  }

Z 方法是 zepto 的构造器(构造方法),拷贝经过筛选命中的 DOM 节点并初始化 length、selector 属性,所以创建出来的对象是个类数组。

zepto.fragment() :

  zepto.fragment = function(html, name, properties) {
    var dom, nodes, container
    // singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/
    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))

    if (!dom) {
      if (html.replace) html = html.replace(tagExpanderRE, "<$1>")
      if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
      if (!(name in containers)) name = '*'
      /**
        * containers = {
        *  'tr': document.createElement('tbody'),
        *  'tbody': table, 'thead': table, 'tfoot': table,
        *  'td': tableRow, 'th': tableRow,
        *  '*': document.createElement('div')
        * },
        */
      container = containers[name]
      container.innerHTML = '' + html
      dom = $.each(slice.call(container.childNodes), function(){
        container.removeChild(this)
      })
    }

    if (isPlainObject(properties)) {
      nodes = $(dom)
      $.each(properties, function(key, value) {
        // methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height‘,‘offset’]
        if (methodAttributes.indexOf(key) > -1) nodes[key](value)
        else nodes.attr(key, value)
      })
    }

    return dom
  }

当 selector 满足是标签的的前提会更细致的检测它是否真正为标签元素,必须要闭合,通过检测之后创建该元素

如果上步骤没有通过检测,会认为用户可能误把不是自闭合的标签写成了自闭合标签如

,会初始化标签为
,标签名字 name 也会重新初始化

创建包裹的 DOM 节点container: 表格元素会做一层向上兼容,如果是表格元素则会创建其父元素,并把当前元素添加到父元素里。是其他未知元素会默认创建 div 元素作为当前元素的父元素

上面的 container.removeChild(this) 里面的 this 可能会使人迷惑,但看完$.each 方法就会明白 this 指向 elements 入参的元素项

借助 container 将 string 类型的元素入参转成真正的 dom 元素,并释放内存

$.each(elements, callback):

  $.each = function(elements, callback){
    var i, key
    if (likeArray(elements)) {
      for (i = 0; i < elements.length; i++)
        if (callback.call(elements[i], i, elements[i]) === false) return elements
    } else {
      for (key in elements)
        if (callback.call(elements[key], key, elements[key]) === false) return elements
    }

    return elements
  }

遍历 elements 参数,执行回掉函数加工 elements 的元素项,当回调函数返回 true 跳出遍历。

likeArray(obj):

function likeArray(obj) {
    var length = !!obj && 'length' in obj && obj.length,
      type = $.type(obj)

    return 'function' != type && !isWindow(obj) && (
      'array' == type || length === 0 ||
        (typeof length == 'number' && length > 0 && (length - 1) in obj)
    )
  }

可用性拉满的一个方法(考虑到了很多场景),检测 obj 是否为一个数组/类数组。

$.type(obj):

$.type = type
function type(obj) {
    return obj == null ? String(obj) :
      // class2type = {},是存放数据类型的对象。
      class2type[toString.call(obj)] || "object"
  }

返回 obj 调用 toString 方法后的数据类型 ‘ [object Object] ... ’

isPlainObject(obj):

  function isPlainObject(obj) {
    return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
  }

判断 obj 是不是普通对象

zepto.qsa(element, selector):

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)
        // simpleSelectorRE = /^[\w-]*$/
    return (element.getElementById && isSimple && maybeID) ? 
      ( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
      (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
      slice.call(
        isSimple && !maybeID && element.getElementsByClassName ? 
          maybeClass ? element.getElementsByClassName(nameOnly) : 
          element.getElementsByTagName(selector) :
          element.querySelectorAll(selector)
      )
  }

提醒:nodeType 为 1、9、11 分别代表 元素节点、document 对象、简化的 document 对象。

兼容 jq 的写法,对 选择器 selector 进行过滤,带 * 或 . 的前缀将会被过滤掉,拿到真正的筛选条件(id/class/tagName/其他)。

如果传递进来的 element 不为节点或 document 返回空数组。
否则根据 selector 类型用相应原生 js 选择器进行 dom 节点的检索并返回。

作者还对 Safari 和 IE9及一下低版本的浏览器做了兼容,但都只是简单的不报错的兼容,在功能 hack 上还是放弃了它们。

find(selector):

  1  find: function(selector){
  2    var result, $this = this
  3    if (!selector) result = $()
  4    else if (typeof selector == 'object')
  5      result = $(selector).filter(function(){
  6        var node = this
  7        return emptyArray.some.call($this, function(parent){
  8          return $.contains(parent, node)
  9        })
  10    })
  11   else if (this.length == 1) result = $(zepto.qsa(this[0], selector))
  12   else result = this.map(function(){ return zepto.qsa(this, selector) })
  13    return result
  14 }

提醒:第二行的 this 指的是命中属性筛选条件的 dom 节点、第六行的 this 指的是 [selector] 数组。

如果 selector 为对象, $(selector) 会返回 [selector] 数组,所以经过 filter 之后
会从命中属性筛选条件的 dom 节点里包含 selector 子节点的节点。

如果 selector 为正常的 string 选择器则会直接使用 js 原生选择器,从命中属性筛选条件的 dom 节点里选找到命中 selector 的节点。

$.contains(parent, node):

  $.contains = document.documentElement.contains ?
    function(parent, node) {
      return parent !== node && parent.contains(node)
    } :
    function(parent, node) {
      while (node && (node = node.parentNode))
        if (node === parent) return true
      return false
    }

判断 node 节点是否为 parent 节点的后代。后代可以是孩子,孙子,曾孙等等

有没有一种套娃的感觉,一个功能点对应一个方法这种低耦合且复用性高的编写代码风格值得学习

ready(callback):

    ready: function(callback){
      // don't use "interactive" on IE <= 10 (it can fired premature)
      if (document.readyState === "complete" ||
          (document.readyState !== "loading" && !document.documentElement.doScroll))
        setTimeout(function(){ callback($) }, 0)
      else {
        var handler = function() {
          document.removeEventListener("DOMContentLoaded", handler, false)
          window.removeEventListener("load", handler, false)
          callback($)
        }
        document.addEventListener("DOMContentLoaded", handler, false)
        window.addEventListener("load", handler, false)
      }
      return this
    },

提醒:document.readyState:当前文档的状态,分别为:uninitialized - 还未开始载入、loading - 载入中、interactive - 已加载,文档与用户可以开始交互、complete - 载入完成
当 readyState 为 interactive 的时候会触发 DOMContentLoaded 事件;
当 readyState 为 complete 的时候会触发 load 事件;
该方法会在页面 '准备好的状态' 下执行传递进来的 callback 方法:
当页面 readyState 为 complete 或 interactive 也就是 DOM 树建成的时候 ‘立即执行' callback 方法,但低版本 IE 浏览器可能会导致 interactive 状态提前,所以需要用 doScroll 配合 load 事件 hack 一下:
IE有个特有的方法doScroll可以检测DOM是否加载完成。 当页面未加载完成时,该方法会报错,直到doScroll不再报错时,就代表DOM加载完成了。
‘立即执行': setTimeout(function(){ callback($) }, 0) 这里的立即执行并不是真正意义上的立即执行而是会把 callback 添加到当前执行队列的尾部等待执行。

compact(array):

  function compact(array) { 
    return filter.call(array, function(item){ return item != null }) 
  }

过滤掉数组为 null 的值

你可能感兴趣的:(从 zepto 源码分析之$构造器的实现)