Zepto 源碼分析 1 - 進入 Zepto

選擇 Zepto 的理由

Zepto is a minimalist JavaScript library for modern browsers with a largely jQuery-compatible API. If you use jQuery, you already know how to use Zepto.

Zepto 是一個用於現代瀏覽器的與 jQuery 大致兼容的 JavaScript 庫。若是你使用過 jQuery 的話,你已經知道如何使用 Zepto 了。css

Zepto.js 官方網對其進行的描述簡而言之即爲一個大致兼容 jQuery 的 JavaScript 庫,所以是否選擇分析 Zepto 這樣的庫前置問題即爲,對於此情此景下的前端入門者(2017 年的前端入行者,Like Me)是否還有分析 jQuery 源碼的必要?html

我心中的答案,即是有也沒有,先說沒有必要的一方面:前端

  • jQuery 包含大量針對「兩套標準」的兼容實現,最明顯的例子便是對 XHR 的處理方式上,所以對於相似的片斷,尋找解決方案已經沒有太大的意義:
var xhrSuccessStatus = {

        // File protocol always yields status code 0, assume 200
        0: 200,

        // Support: IE <=9 only
        // #1450: sometimes IE returns 1223 when it should be 204
        1223: 204
    },
  • jQuery 處理邏輯的基本對象基於 DOM,所以同時包含基礎庫 Sizzle.js 用以實現純粹的跨平臺選擇器實現,但因爲 Document.querySelectorAll() 的兼容實現已經覆蓋全部現代瀏覽器,相似這樣的判斷也沒有了太多的借鑑價值:
if ( support.qsa &&
    !nonnativeSelectorCache[ selector + " " ] &&
    (!rbuggyQSA || !rbuggyQSA.test( selector )) &&
    
    // Support: IE 8 only
    // Exclude object elements
    (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") )

-再說有必要的一方面,仍然拿 querySelectorAll 舉例,Zepto 這一 jQuery 兼容庫中將其封裝爲了以下的 qsa 函數:node

zepto.qsa = function(element, selector) {
  //\ ...
      return element.getElementById && isSimple && maybeID
      ? (found = element.getElementById(nameOnly))
      //\ ...
      : slice.call(
          isSimple && !maybeID && element.getElementsByClassName
            ? maybeClass
              ? element.getElementsByClassName(nameOnly)
              : element.getElementsByTagName(selector)
            : element.querySelectorAll(selector)
        );
  };

即在調用該函數時,根據類型先作出判斷,若是目標爲可識別的類型,那麼採起相應的方法進行選擇,最終降級到使用 querySelectorAll() 這一函數進行選擇。採用該方法的目的即爲了儘量的提升選擇器性能,參考 jsperf getElementById vs querySelector 的運行結果即可略知一二:webpack

測試調用 Ops / Sec Ops 差距
document.getElementById("foo") 27,869,670 fastest
document.querySelector("#foo") 11,206,845 62% slower
document.querySelector("[id=foo]") 11,281,576 59% slower

另外一個例子,關於 jQuery 對象本質的問題,以簡化的 Zepto 爲例,其擁有以下多種的初始化方式:git

$()
$(selector, [context])  ⇒ collection
$(<Zepto collection>)  ⇒ same collection
$(<DOM nodes>)  ⇒ collection
$(htmlString)  ⇒ collection
$(htmlString, attributes)  ⇒ collection v1.0+
Zepto(function($){ ... })

而其調用生成的對象僅僅形似一個數組,但卻如何實現能夠簡單的操做 DOM 元素:github

// 構造一個空的 Zepto(Z) 對象
> $()

[selector: ""]
-> selector: ""
-> length: 0
-> __proto__: Object

// 選擇並着色
> $("p:last-child").css('background-color', 'aliceblue')

答案便存在於 Zepto 的模塊結構當中,$.fn 包含暴露在外的工具函數,當一個 Z 對象建立時將其設定爲原型沒,即可獲取其中的工具函數:web

zepto.Z.prototype = Z.prototype = $.fn;

這樣設計思想與實現方式的例子在 jQuery/Zepto 中比比皆是。面對有仍是沒有必要閱讀 jQuery 源碼的問題,比起 jQuery 中寫滿瀏覽器崢嶸歷史的長篇鉅著,Zepto 將其簡化爲了:ajax

  • 主體部分 1000 行的代碼規模
  • 高度的 jQuery API 兼容
  • 模塊化的組合方式,默認編譯方式只包含現代瀏覽器支持

最大限度的下降了閱讀成本,所以該問題的答案,能夠回答爲去粗取精,經過分析一種 jQuery 20% 代碼量的最簡核心實現(Zepto),領略其 80% 的設計思想shell

環境搭建

Zepto 分析環境的搭建僅須要一個常規頁面原始代碼

  • 一個常規頁面:打開 Zepto 的首頁便可,在開發人員工具中便可使用 Zepto
  • 原始代碼:本篇分析的 Zepto 代碼參照 Commit 3172e9,進入該代碼分支中便可。

熱身部分,先從 Zepto 的模塊結構開始:

模塊結構

Zepto 核心部分包含 zepto module 等五個默認編譯入生產代碼的模塊,而且給出了擴展插件的方法,那麼第一個問題即是,Zepto 是如何引入並提供模塊結構的

  • 進入 package.json 發現 Zepto 項目提供了三個公用的命令行入口,供 coffee-script 使用,實際入口爲 make
"scripts": {
        "test": "coffee make test",
        "dist": "coffee make dist",
        "start": "coffee test/server.coffee"
    },
  • 進入 make 文件,直接找到它生成 dist 的過程:
//\ Line 33
target.dist = ->
  target.build()
  target.minify()
  target.compress()

target.build = ->
  cd __dirname
  mkdir '-p', 'dist'

  //\ 這裏標明瞭 5 個默認的編譯模塊,能夠經過環境變量改變編譯目標,且這些模塊名稱即爲對應的文件名
  modules = (env['MODULES'] || 'zepto event ajax form ie').split(' ')
  module_files = ( "src/#{module}.js" for module in modules )
  
  //\ 聲明許可證,放在 dist 頭部,將目標源碼文件中的註釋刪除,將 3 行以上的空行轉換爲兩個空行寫入
  intro = "/* Zepto #{describe_version()} - #{modules.join(' ')} - zeptojs.com/license */\n"
  dist = cat(module_files).replace(/^\/[\/*].*$/mg, '').replace(/\n{3,}/g, "\n\n")
  
  //\ 判斷是否將 AMD Layout 寫入,若是是,則將上文代碼填入 AMD 代碼段中,回報體積
  dist = cat('src/amd_layout.js').replace(/YIELD/, -> dist.trim()) unless env['NOAMD']
  (intro + dist).to(zepto_js)
  report_size(zepto_js)
  • 幾點額外的補充:

    • make 中的 #!/usr/bin/env coffee 是類 Unix 操做系統中表示文本文件解析器的聲明 Shebang),相似於 HTML 文檔的 DOCTYPE,該文件寫法可明顯看出 Linux Shell 腳本的意味,不過採用了 shelljs 這一運行在 Nodejs 上的庫進行了跨平臺。
    • make 中的 compress 過程當中用到了 gzip 進行壓縮: inp.pipe(gzip).pipe(out),該項壓縮對於用戶是透明的,用戶瀏覽器能夠經過 Content-Encoding HTTP 字段獲知該文件已被壓縮過並提供預解壓操做,詳見 MDN - HTTP 協議中的數據壓縮
    • make 腳本中還有不少的借鑑之處,例如 102 行 git 相關操做,以及 108 行開始調用 uglify.js 進行代碼壓縮的過程等,不管用 gruntwebpack 組織流水線也只是類似的工序,提供多個出口。
  • 對於 src/amd_layout 這一沒被列入模塊列表中的文件,其做用即爲兼容 AMD 標準
//\ src/amd_layout
//\ 做用於全局的 IIFE
(function(global, factory) {

  //\ AMD 兼容寫法
  if (typeof define === 'function' && define.amd)
    define(function() { return factory(global) })
  else
    factory(global)
}(window, function(window) {

  //\ 若是包含 AMD 編譯,就將上文代碼完整寫入該函數
  YIELD
  return Zepto
}))

此模塊忽略 AMD 相關邏輯會獲得一個 JavaScript 庫中常見的當即執行表達式(IIFE)結構,使用該結構的目的即爲構造塊級做用域,防止庫中的變量對全局進行污染,是一種很是經常使用的包裝方法,詳見 MDN - IIFE

(function(global, factory) {
  //\ ...
}(
  //\ ...
))

進入 Zepto 主模塊

分析完 Zepto 模塊結構,開始進入 Zepto 主模塊,主模塊框架以下:

//\ src/zepto.js

//\ 將 Zepto 定義爲一個 IIFE
var Zepto = (function() {
  //\... Zepto 內部變量定義部分 6-47 行

  //\... Zepto 內部對象 zepto / 公共函數定義部分 48-167 行

  //\... zepto/Z/$ 關係構造
  zepto.Z =
  zepto.isZ =
  zepto.init =
  $ =
  extend =
  $.extend =
  
  //\... 與 DOM 的橋樑 - querySelectorAll 實現
  zepto.qsa =
  
  //\ $ 對象對外的工具函數 279-404 行
  
  //\ 定義 $.fn,經過原型鏈賦予方法 
  $.fn = {
    constructor: zepto.Z,
    //\ ...
  }
  
  //\ 一些對於 $.fn 的其餘函數實現 852-936 行
  
  //\ 繼續處理 zepto/Z/$ 的關係,將 Z 的原型指向 $.fn
  zepto.Z.prototype = Z.prototype = $.fn

  zepto.uniq = uniq
  zepto.deserializeValue = deserializeValue
  
  //\ 將內置對象 zepto 掛載到 $ 對象
  $.zepto = zepto

  //\ 將 $ 做爲 IIFE 返回值返回
  return $
})()

//\ 將 window.Zepto 指向 Zepto IIFE 的返回值 '$'
window.Zepto = Zepto

//\ 若是 '$' 還未被佔用,就將其也初始爲 Zepto IIFE 的返回值 '$'
window.$ === undefined && (window.$ = Zepto)

其核心設計思想即體如今 zepto/Z/$ 這三個組件之間的關係上,處理他們的關係也正是本篇的目的所在。

相關文章
相關標籤/搜索