天天看點

zepto.js源碼解讀(二):zepto.init函數

zepto.init函數,源碼也就幾十行:

zepto.init = function(selector, context) {
    var dom
    // If nothing given, return an empty Zepto collection
    if (!selector) return zepto.Z()
    // Optimize for string selectors
    else if (typeof selector == 'string') {
      selector = selector.trim()
      // If it's a html fragment, create nodes from it
      // Note: In both Chrome 21 and Firefox 15, DOM error 12
      // is thrown if the fragment doesn't begin with <
      if (selector[] == '<' && fragmentRE.test(selector))
        dom = zepto.fragment(selector, RegExp.$, context), selector = null
      // If there's a context, create a collection on that context first, and select
      // nodes from there
      else if (context !== undefined) return $(context).find(selector)
      // If it's a CSS selector, use it to select nodes.
      else dom = zepto.qsa(document, selector)
    }
    // If a function is given, call it when the DOM is ready
    else if (isFunction(selector)) return $(document).ready(selector)
    // If a Zepto collection is given, just return it
    else if (zepto.isZ(selector)) return selector
    else {
      // normalize array if an array of nodes is given
      if (isArray(selector)) dom = compact(selector)
      // Wrap DOM nodes.
      else if (isObject(selector))
        dom = [selector], selector = null
      // If it's a html fragment, create nodes from it
      else if (fragmentRE.test(selector))
        dom = zepto.fragment(selector.trim(), RegExp.$, context), selector = null
      // If there's a context, create a collection on that context first, and select
      // nodes from there
      else if (context !== undefined) return $(context).find(selector)
      // And last but no least, if it's a CSS selector, use it to select nodes.
      else dom = zepto.qsa(document, selector)
    }
    // create a new Zepto collection from the nodes found
    return zepto.Z(dom, selector)
  }
           

可以看到這個函數可以傳入selector和context兩個參數。

對照上面代碼我們從init函數内部一步一步往下解讀。

定義一個dom變量。

  1. 如果傳的selector為空,則傳回一個空的Zepto集合。
  2. 如果傳入的selector是字元串類型的,對selector使用trim()方法去除字元串兩邊空格。
    • 如果selector的首位是<并且符合fragmentRE正則的比對規則,即:它是代碼中的第一個html标簽或者注釋。(dom = zepto.fragment(selector, RegExp.$1, context)),就能生成dom節點,并selector置null。
    • 如果傳入了content參數,就新建立一個zepto對象($(content)),并從中find(查找)這個selector節點,并傳回。
    • 如果不符合上面的情況,即傳入的selector是個css選擇器,dom就是在整個文檔裡查找到的selector節點。
  3. 如果傳入的selector是個函數類型(isArray(selector))。它的傳回結果是:dom樹渲染完成後,執行這個函數。
  4. 如果傳入的本來就是是一個Zepto集(zepto.isZ(selector)),直接傳回它本身。
  5. 如果
    1. 傳入的selector是數組類型(isArray(selector)),dom = compact(selector);
    2. 傳入的selector是對象類型(isObject(selector)),就把節點selector放在一個數組裡,賦給dom。selector置空(這樣就不影響後續selector的判斷).
    3. 如果selector的首位是<并且符合fragmentRE正則的比對規則,即:它是代碼中的第一個html标簽或者注釋。(dom = zepto.fragment(selector, RegExp.$1, context)),就能生成dom節點,selector置空。
    4. 如果傳入了content參數,就新建立一個zepto對象($(content)),并從中find(查找)這個selector節點,并傳回。
  6. 把最終得到的dom和selector當參數傳給zepto.z()函數。
  7. zepto.init最終的傳回值就是 zepto.z(dom, selector)的結果。

也就是說其實zepto.init的結果就是傳回這個zepto.z()函數的值。

上面比較籠統,因為那些加紅部分,有的實在不了解它到底是什麼含義,是以無法很好的去了解。

源碼的10-14行定義了幾個正則規則,我們有必要一一了解。

  • fragmentRE = /^\s*<(\w+|!)[^>]*>/

    以出現任意次的空格字元開頭,<,至少出現一次的單詞字元 或者 一個!,出現任意次的(除去>)的字元,>。這個規則要比對的是:代碼段中的第一個html标簽,或者注釋。比如:

    <!DOCTYPE html>

    或者

    <!-- 這是注釋 -->

  • singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/

    顧名思義,這是一個單标簽比對正則。以<開頭 ,至少出現一次的字母字元,不定次的空格字元,出現0或1次的/,<,/, 反向引用符合比對規則的第一個字元串(?:比對的時候不捕獲該分組)。這個規則要比對的是:

    < img / >

    或者

    <div></div>

    這樣的标簽,無法比對

    <img src="#"/ >

    ,

    <div>lhchweb</div>

    等這樣的标簽
  • rootNodeRE = /^(?:body|html)$/i

    。比對的是根節點,忽略大小寫。以body或者html開頭,以body或者html結束的(不捕獲該分組)
  • apitalRE = /([A-Z])/g

    。全局比對大寫字母。
  • tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig

    。以area|br|col|embed|hr|img|input|link|meta|param開頭的斷言的後顧(相對于前瞻,即排除以上這些标簽),至少出現一次的[\w:](單詞字元冒号),出現任意次的字元(排除>),/,>。全局比對,忽略大小寫。這個規則比對一個開始/結束标簽不能比對

    <img />

    <input />

    等這樣的自封閉标簽

語言描述可能不夠到位精準,建議結合代碼自己分析一下。

zepto.fragment()函數也多次出現,看下代碼:

// `$.zepto.fragment` takes a html string and an optional tag name
  // to generate DOM nodes from the given html string.
  // The generated DOM nodes are returned as an array.
  // This function can be overridden in plugins for example to make
  // it compatible with browsers that don't support the DOM fully.
  zepto.fragment = function(html, name, properties) {
    var dom, nodes, container

    // A special case optimization for a single tag
    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$))

    if (!dom) {
      if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
      if (name === undefined) name = fragmentRE.test(html) && RegExp.$
      if (!(name in containers)) name = '*'

      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) {
        if (methodAttributes.indexOf(key) > -) nodes[key](value)
        else nodes.attr(key, value)
      })
    }

    return dom
  }
           

注釋裡說:zepto.fragment方法能夠根據給定的參數生成DOM節點。

我們還是一步一步來看:

  1. zepto.fragment()可傳入html, name, properties等作為參數。
  2. 定義dom, nodes, container
  3. 如果傳入的html符合singleTagRE正則比對規則 即它是單标簽,就把它轉為zepto對象,指派給dom,

    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))

  4. 如果不符合上面的正則:
    • 傳入的html參數有replace方法。就把該參數中符合tagExpanderRE正則規則的第一個第二個結果反向引用。生成标簽對。把參數html替換為這個标簽對。比如:傳入的是不完整的

      <span/>

      那麼

      html = <span></span>

      或者傳入的是

      <p abc/>

      那麼

      html = <p abc></p>

    • 如果未傳入參數name但是給定的html參數符合fragmentRE正則,即它是代碼段中的第一個标簽,就把這個标簽名賦給name。
    • 傳入了name參數,并且name參數不在給定的containers對象的屬性裡,就把‘*’(值為*表示會成為一個div标簽)賦給name。container = containers[name](這樣就能生成類似于:

      <div>,<tbody>

      這樣的标簽,下面有介紹)

      container.innerHtml = ‘’ + html

      .調用$.each方法對container進行處理(源碼151-156:先把container的子集轉化為數組,并周遊,然後移除它的子集)進而得到dom。
    • 如果傳入的properties參數是個純粹的對象(isPlainObject字面意思看),把上面得到的dom轉化為zepto對象。

      nodes = $(dom)

      。對properties對象調用$.each方法周遊(key,value)(如果傳入了一些屬性和對應的屬性值(他們以鍵值對的方式存在于properties對象中),就去查找屬性是否在methodAttributes裡,在的話得到這個屬性值。不在的話就給其設定屬性,值為對應的屬性值)。
    • 如果properties對象的屬性存在于methodAttributes(源碼17行,定義了一些特殊的屬性:

      ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset']

      )這個數組中,那麼就得到這個屬性值(

      nodes[key](value)

      )。比如

      $('div')['css']('height')

    • 反之,就給nodes節點設定屬性和屬性值(

      nodes.attr(key, value)

      )。比如top不在上述數組中,就給節點設定top屬性,值為對應的值。

藍字部分的containers,在源碼22行是這樣定義的:

containers = {
      'tr': document.createElement('tbody'),
      'tbody': table, 'thead': table, 'tfoot': table,
      'td': tableRow, 'th': tableRow,
      '*': document.createElement('div')
    }
           

它指定了一些特定建立方法。比如tr就建立為tbody,不在containers屬性裡的都建立為div(

'*': document.createElement('div')

)。

總結起來就是:

zepto.fragment方法會根據傳入參數的不同,最終給我們傳回dom。傳回的這個dom呢,總結起來就是兩種情況:

  1. 如果是單标簽,dom就是一個zepto對象(

    dom = $(document.createElement(RegExp.$1))

  2. 如果不是,那麼它就是包含這些dom節點的數組(

    dom = $.each(slice.call(container.childNodes),function()container.removeChild(this) })

    )。

而這和注釋裡的解釋是一緻的。

至于上面的isArray()方法,isObject()方法,isArray()方法,compact()方法,zepto.isZ()方法等不影響整體了解,篇幅原因不一一介紹(其實我都是從字面意思去了解他們的作用,再去查找有關的代碼驗證自己的猜測)。

再回頭梳理一遍zepto.init方法:

它能接受兩個參數(selector, context),根據傳入的參數的不同分以下情況:

if (!selector) //...
else if (typeeof selector == 'string') {
    if (selector[] == '<' && fragmentRE.test(selector)) //....
    else if (context !== undefined) //...
    else   //...
}
else if (isFunction(selector)) //...
else if (zepto.isZ(selector))  //...
else {
    if (isArray(selector))  //...
    else if (isObject(selector))  //...
    else if (context !== undefined) //...
    else  //...
}
           

不管分了多少種情況,總能得到dom和selector。然後把得到的dom和selector作為參數傳入zepto.Z()函數,經過zepto.Z()的處理後,傳回這個處理結果。

那麼 zepto.Z()是什麼,又做了那些事情,下篇文章介紹。