讀 Zepto 源碼系列


雖然最近工作中沒有怎么用 zepto ,但是據說 zepto 的源碼比較簡單,而且網上的資料也比較多,所以我就挑了 zepto 下手,希望能為以后閱讀其他框架的源碼打下基礎吧。

源碼版本

本文閱讀的源碼為 zepto1.2.0

閱讀zepto之前需要了解 javascript 原型鏈和閉包的知識,推薦閱讀王福朋的這篇文章:深入理解 Javascript 原型和閉包,寫得很詳細,也非常易於閱讀。

源碼結構

整體結構

var Zepto = (function () { ... })() window.Zepto = Zepto window.$ === undefined && (window.$ = Zepto)

如果在編輯器中將 zepto 的源碼折疊起來,看到的就跟上面的代碼一樣。

zepto 的核心是一個閉包,加載完畢后立即執行。然后暴露給全局變量 zepto ,如果 $ 沒有定義,也將 $ 賦值為 zepto 。

核心結構

在這部分中,我們先不關注 zepto 的具體實現,只看核心的結構,因此我將zepto中的邏輯先移除,得出如下的核心結構:

var zepto = {}, $ function Z(doms) { var len = doms.length for (var i = 0; i < len; i++) { this[i] = doms[i] } this.length = doms.length } zepto.Z = function(doms) { return new Z(doms) } zepto.init = function(doms) { var doms = ['domObj1','domObj2','domObj3'] return zepto.Z(doms) } $ = function() { return zepto.init() } $.fn = { constructor: zepto.Z, method: function() { return this } } zepto.Z.prototype = Z.prototype = $.fn return $

在源碼中,可以看出, $ 其實是一個函數,同時在 $ 身上又掛了很多屬性和方法(這里體現在 $.fn 身上,其他的會在后續的文章中談到)。

我們在使用 zepto 的時候,會用 $ 去獲取 dom ,並且在這些 dom 對象身上都有 zepto 定義的各種各樣的操作方法。

從上面的偽代碼中,可以看到,$ 其實調用了 zepto.init() 方法,在 init 方法中,會獲取到 dom 元素集合,然后將集合交由 zepto.Z() 方法處理,而 zepto.Z 方法返回的是函數 Z 的一個實例。

函數 Z 會將 doms 展開,變成實例的屬性,key 為對應 domObj 的索引, 並且設置實例的 length 屬性。

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

讀到這里,你可能會有點疑惑,$ 最終返回的是 Z 函數的實例,但是 Z 函數明明沒有 dom 的操作方法啊,這些操作方法都定義在 $.fn 身上,為什么 $ 可以調用這些方法呢?

其實關鍵在於這句代碼 Z.prototype = $.fn ,這句代碼將 Z 的 prototype 指向 $.fn ,這樣,Z 的實例就繼承了 $.fn 的方法。

既然這樣就已經讓 Z 的實例繼承了 $.fn 的方法,那 zepto.Z.prototype = $.fn 又是為什么呢?

如果我們再看源碼,會發現有這樣的一個方法:

zepto.isZ = function(object) { return object instanceof zepto.Z }

這個方法是用來判讀一個對象是否為 zepto 對象,這是通過判斷這個對象是否為 zepto.Z 的實例來完成的,因此需要將 zepto.Z 和 Z 的 prototype 指向同一個對象。 isZ 方法會在 init 中用到,后面也會介紹。

 

 

 

 

 

數組方法

定義

var emptyArray = [] concat = emptyArray.concat filter = emptyArray.filter slice = emptyArray.slice

zepto 一開始就定義了一個空數組 emptyArray,定義這個空數組是為了取得數組的 concatfilterslice 方法

compact

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

刪除數組中的 null 和 undefined

這里用的是數組的 filter 方法,過濾出 item != null 的元素,組成新的數組。這里刪除掉 null 很容易理解,為什么還可以刪除 undefined 呢?這是因為這里用了 != ,而不是用 !== ,用 != 時, null 各 undefined 都會先轉換成 false 再進行比較。

關於 null 和 undefined 推薦看看這篇文章: undefined與null的區別

flatten

function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array }

將數組扁平化,例如將數組 [1,[2,3],[4,5],6,[7,[89]] 變成 [1,2,3,4,5,6,7,[8,9]] ,這個方法只能展開一層,多層嵌套也只能展開一層。

這里,我們先把 $.fn.concat 等價於數組的原生方法 concat,后面的章節也會分析 $.fn.concat 的。

這里比較巧妙的是利用了 apply ,apply 會將 array 中的 item 當成參數,concat.apply([], [1,2,3,[4,5]]) 相當於 [].concat(1,2,3,[4,5]),這樣數組就扁平化了。

uniq

uniq = function(array) { return filter.call(array, function(item, idx) { return array.indexOf(item) == idx }) }

數組去重。

數組去重的原理是檢測 item 在數組中第一次出現的位置是否和 item 所處的位置相等,如果不相等,則證明不是第一次出現,將其過濾掉。

字符串方法

camelize

camelize = function(str) { return str.replace(/-+(.)?/g, function(match, chr) { return chr ? chr.toUpperCase() : '' }) }

將 word-word 的形式的字符串轉換成 wordWord 的形式, - 可以為一個或多個。

正則表達式匹配了一個或多個 - ,捕獲組是捕獲 - 號后的第一個字母,並將字母變成大寫。

dasherize

function dasherize(str) { return str.replace(/::/g, '/') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') .replace(/([a-z\d])([A-Z])/g, '$1_$2') .replace(/_/g, '-') .toLowerCase() }

將駝峰式的寫法轉換成連字符 - 的寫法。

例如 a = A6DExample::Before

第一個正則表達式是將字符串中的 :: 替換成 / 。a 變成 A6DExample/Before

第二個正則是在出現一次或多次大寫字母和出現一次大寫字母和連續一次或多次小寫字母之間加入 _a 變成 A6D_Example/Before

第三個正則是將出現一次小寫字母或數字和出現一次大寫字母之間加上 _a 變成A6_D_Example/Before

第四個正則表達式是將 _ 替換成 -a 變成A6-D-Example/Before

最后是將所有的大寫字母轉換成小寫字母。a 變成 a6-d-example/before

我對正則不太熟悉,正則解釋部分參考自:zepto源碼--compact、flatten、camelize、dasherize、uniq--學習筆記

數據類型檢測

定義

class2type = {},
toString = class2type.toString, // Populate the class2type map $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type["[object " + name + "]"] = name.toLowerCase() })

$.each 函數后面的文章會講到,這段代碼是將基本類型掛到 class2type 對象上。class2type 將會是如下的形式:

class2type = {
  "[object Boolean]": "boolean", "[object Number]": "number" ... } 

type

function type(obj) { return obj == null ? String(obj) : class2type[toString.call(obj)] || "object" }

type 函數返回的是數據的類型。

如果 obj == null ,也就是 null 和 undefined,返回的是字符串 null 或 undefined

否則調用 Object.prototype.toString (toString = class2type.toString)方法,將返回的結果作為 class2type 的 key 取值。Object.prototype.toString 對不同的數據類型會返回形如 [object Boolean] 的結果。

如果都不是以上情況,默認返回 object 類型。

isFunction & isObject

function isFunction(value) { return type(value) === 'function' } function isObject(obj) { return type(obj) == 'object' }

調用 type 函數,判斷返回的類型字符串,就知道是什么數據類型了

isWindow

function isWindow(obj) { return obj != null && obj == obj.window }

判斷是否為瀏覽器的 window 對象

要為 window 對象首先要滿足的條件是不能為 null 或者 undefined, 並且 obj.window 為自身的引用。

isDocument

function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }

判斷是否為 document 對象

節點上有 nodeType 屬性,每個屬性值都有對應的常量。document 的 nodeType 值為 9 ,常量為 DOCUMENT_NODE

具體見:MDN文檔:Node.nodeType

isPlainObject

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

判斷是否為純粹的對象

純粹對象首先必須是對象 isObject(obj)

並且不是 window 對象 !isWindow(obj)

並且原型要和 Object 的原型相等

isArray

isArray = Array.isArray || function(object) { return object instanceof Array}

這個方法來用判斷是否為數組類型。

如果瀏覽器支持數組的 isArray 原生方法,就采用原生方法,否則檢測數據是否為 Array 的實例。

我們都知道,instanceof 的檢測的原理是查找實例的 prototype 是否在構造函數的原型鏈上,如果在,則返回 true。 所以用 instanceof 可能會得到不太准確的結果。例如:

index.html

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <script>  window.onload = function () {  var fwindow = window.framePage.contentWindow // frame 頁面的window對象  var fArray = fwindow.Array // frame 頁面的Array  var fdata = fwindow.data // frame 頁面的 data [1,2,3]  console.log(fdata instanceof fArray) // true  console.log(fdata instanceof Array) // false  }  </script> <title>Document</title> </head> <body> <iframe id="framePage" src="frame.html" frameborder="0"></iframe> </body> </html>

frame.html

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script>  window.data = [1,2,3]  </script> </head> <body> <p>frame page</p> </body> </html>

由於 iframe 是在獨立的環境中運行的,所以 fdata instanceof Array 返回的 false 。

在 MDN 上看到,可以用這樣的 ployfill 來使用 isArray

if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]' } }

也就是說,isArray 可以修改成這樣:

isArray = Array.isArray || function(object) { return Object.prototype.toString.call(object) === '[object Array]'}

為什么 zepto 不這樣寫呢?知道的可以留言告知下。

likeArray

function likeArray(obj) { var length = !!obj && // obj必須存在 'length' in obj && // obj 中必須存在 length 屬性 obj.length, // 返回 length的值 type = $.type(obj) // 調用 type 函數,返回 obj 的數據類型。這里我有點不太明白,為什么要覆蓋掉上面定義的 type 函數呢?再定義多一個變量,直接調用 type 函數不好嗎? return 'function' != type && // 不為function類型 !isWindow(obj) && // 並且不為window類型 ( 'array' == type || length === 0 || // 如果為 array 類型或者length 的值為 0,返回true (typeof length == 'number' && length > 0 && (length - 1) in obj) // 或者 length 為數字,並且 length的值大於零,並且 length - 1 為 obj 的 key ) }

判斷是否為數據是否為類數組。

類數組的形式如下:

likeArrayData = {
  '0': 0, '1': 1, "2": 2 length: 3 }

可以看到,類數組都有 length 屬性,並且 key 為按0,1,2,3 順序的數字。

代碼已經有注釋了,這里再簡單總結下

首先將 function類型和 window 對象排除

再將 type 為 array 和 length === 0 的認為是類數組。type 為 array 比較容易理解,length === 0 其實就是將其看作為空數組。

最后一種情況必須要滿足三個條件:

  1. length 必須為數字
  2. length 必須大於 0 ,表示有元素存在於類數組中
  3. key length - 1 必須存在於 obj 中。我們都知道,數組最后的 index 值為 length -1 ,這里也是檢查最后一個 key 是否存在。

 

 

 

Zepto 提供了豐富的工具函數,下面來一一解讀。

源碼版本

本文閱讀的源碼為 zepto1.2.0

$.extend

$.extend 方法可以用來擴展目標對象的屬性。目標對象的同名屬性會被源對象的屬性覆蓋。

$.extend 其實調用的是內部方法 extend, 所以我們先看看內部方法 extend 的具體實現。

function extend(target, source, deep) { for (key in source) // 遍歷源對象的屬性值 if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { // 如果為深度復制,並且源對象的屬性值為純粹對象或者數組 if (isPlainObject(source[key]) && !isPlainObject(target[key])) // 如果為純粹對象 target[key] = {} // 如果源對象的屬性值為純粹對象,並且目標對象對應的屬性值不為純粹對象,則將目標對象對應的屬性值置為空對象 if (isArray(source[key]) && !isArray(target[key])) // 如果源對象的屬性值為數組,並且目標對象對應的屬性值不為數組,則將目標對象對應的屬性值置為空數組 target[key] = [] extend(target[key], source[key], deep) // 遞歸調用extend函數 } else if (source[key] !== undefined) target[key] = source[key] // 不對undefined值進行復制 }

extend 的第一個參數 taget 為目標對象, source 為源對象, deep 表示是否為深度復制。當 deep 為 true 時為深度復制, false 時為淺復制。

  1. extend 函數用 for···in 對 source 的屬性進行遍歷

  2. 如果 deep 為 false 時,只進行淺復制,將 source 中不為 undefined 的值賦值到 target 對應的屬性中(注意,這里用的是 !==,不是 != ,所以只排除嚴格為 undefined 的值,不包含 null )。如果 source 對應的屬性值為對象或者數組,會保持該對象或數組的引用。

  3. 如果 deep 為 true ,並且 source 的屬性值為純粹對象或者數組時

    3.1. 如果 source 的屬性為純粹對象,並且 target 對應的屬性不為純粹對象時,將 target 的對應屬性設置為空對象

    3.2. 如果 source 的屬性為數組,並且 target 對應屬性不為數組時,將 target 的對應屬性設置為空數組

    3.3. 將 source 和 target 對應的屬性及 deep 作為參數,遞歸調用 extend 函數,以實現深度復制。

現在,再看看 $.extend 的具體實現

$.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 }

在說原理之前,先來看看 $.extend 的調用方式,調用方式如下:

$.extend(target, [source, [source2, ...]]) 或 $.extend(true, target, [source, ...])

在 $.extend 中,如果不需要深度復制,第一個參數可以是目標對象 target, 后面可以有多個 source 源對象。如果需要深度復制,第一個參數為 deep ,第二個參數為 target ,為目標對象,后面可以有多個 source 源對象。

$.extend 函數的參數設計得很優雅,不需要深度復制時,可以不用顯式地將 deep 置為 false。這是如何做到的呢?

在 $.extend 函數中,定義了一個數組 args,用來接受除第一個參數外的所有參數。

然后判斷第一個參數 target 是否為布爾值,如果為布爾值,表示第一個參數為 deep ,那么第二個才為目標對象,因此需要重新為 target 賦值為 args.shift() 。

最后就比較簡單了,循環源對象數組 args, 分別調用 extend 方法,實現對目標對象的擴展。

$.each

$.each 用來遍歷數組或者對象,源碼如下:

$.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 }

先來看看調用方式:$.each(collection, function(index, item){ ... })

$.each 接收兩個參數,第一個參數 elements 為需要遍歷的數組或者對象,第二個 callback 為回調函數。

如果 elements 為數組,用 for 循環,調用 callback ,並且將數組索引 index 和元素值 item 傳給回調函數作為參數;如果為對象,用 for···in 遍歷屬性值,並且將屬性 key 及屬性值傳給回調函數作為參數。

注意回調函數調用了 call 方法,call 的第一個參數為當前元素值或當前屬性值,所以回調函數的上下文變成了當前元素值或屬性值,也就是說回調函數中的 this 指向的是 item 。這在dom集合的遍歷中相當有用。

在遍歷的時候,還對回調函數的返回值進行判斷,如果回調函數返回 false (if (callback.call(elements[i], i, elements[i]) === false)) ,立即中斷遍歷。

$.each 調用結束后,會將遍歷的數組或對象( elements )返回。

$.map

可以遍歷數組(類數組)或對象中的元素,根據回調函數的返回值,將返回值組成一個新的數組,並將該數組扁平化后返回,會將 null 及 undefined 排除。

$.map = function(elements, callback) { var value, values = [], i, key if (likeArray(elements)) for (i = 0; i < elements.length; i++) { value = callback(elements[i], i) if (value != null) values.push(value) } else for (key in elements) { value = callback(elements[key], key) if (value != null) values.push(value) } return flatten(values) }

先來看看調用方式: $.map(collection, function(item, index){ ... })

elements 為類數組或者對象。callback 為回調函數。當為類數組時,用 for 循環,當為對象時,用 for···in 循環。並且將對應的元素(屬性值)及索引(屬性名)傳遞給回調函數,如果回調函數的返回值不為 null 或者 undefined ,則將返回值存入新數組中,最后將新數組扁平化后返回。

$.camelCase

該方法是將字符串轉換成駝峰式的字符串

$.camelCase = camelize

$.camelCase 調用的是內部方法 camelize ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述,本篇文章就不再展開。

$.contains

用來檢查給定的父節點中是否包含有給定的子節點,源碼如下:

$.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 }

先來看看調用:$.contains(parent, node)

參數 parent 為父子點,node 為子節點。

$.contains 的主體是一個三元表達式,返回的是一個匿名函數。三元表達式的條件是 document.documentElement.contains, 用來檢測瀏覽器是否支持 contains 方法,如果支持,則直接調用 contains 方法,並且將 parent 和 node 為同一個元素的情況排除。

否則,返回另一外匿名函數。該函數會一直向上尋找 node 元素的父元素,如果能找到跟 parent 相等的父元素,則返回 true, 否則返回 false

$.grep

該函數其實就是數組的 filter 函數

  $.grep = function(elements, callback) { return filter.call(elements, callback) }

從源碼中也可以看出,$.grep 調用的就是數組方法 filter

$.inArray

返回指定元素在數組中的索引值

 $.inArray = function(elem, array, i) { return emptyArray.indexOf.call(array, elem, i) }

先來看看調用 $.inArray(element, array, [fromIndex])

第一個參數 element 為指定的元素,第二個參數為 array 為數組, 第三個參數 fromIndex 為可選參數,表示從哪個索引值開始向后查找。

$.inArray 其實調用的是數組的 indexOf 方法,所以傳遞的參數跟 indexOf 方法一致。

$.isArray

判斷是否為數組

$.isArray = isArray

$.isArray 調用的是內部方法 isArray ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述。

$.isFunction

判讀是否為函數

$.isFunction = isFunction

$.isFunction 調用的是內部方法 isFunction ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述。

$.isNumeric

是否為數值

$.isNumeric = function(val) { var num = Number(val), // 將參數轉換為Number類型 type = typeof val return val != null && type != 'boolean' && (type != 'string' || val.length) && !isNaN(num) && isFinite(num) || false }

判斷是否為數值,需要滿足以下條件

  1. 不為 null
  2. 不為布爾值
  3. 不為NaN(當傳進來的參數不為數值或如'123'這樣形式的字符串時,都會轉換成NaN)
  4. 為有限數值
  5. 當傳進來的參數為字符串的形式,如'123' 時,會用到下面這個條件來確保字符串為數字的形式,而不是如 123abc 這樣的形式。(type != 'string' || val.length) && !isNaN(num) 。這個條件的包含邏輯如下:如果為字符串類型,並且為字符串的長度大於零,並且轉換成數組后的結果不為NaN,則斷定為數值。(因為 Number('') 的值為 0

$.isPlainObject

是否為純粹對象,即以 {} 常量或 new Object() 創建的對象

$.isPlainObject = isPlainObject

$.isPlainObject 調用的是內部方法isPlainObject ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述。

$.isWindow

是否為瀏覽器的 window 對象

$.isWindow = isWindow

$.isWindow 調用的是內部方法 isWindow ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述。

$.noop

空函數

$.noop = function() {}

這個在需要傳遞回調函數作為參數,但是又不想在回調函數中做任何事情的時候會非常有用,這時,只需要傳遞一個空函數即可。

$.parseJSON

將標准JSON格式的字符串解釋成JSON

if (window.JSON) $.parseJSON = JSON.parse

其實就是調用原生的 JSON.parse, 並且在瀏覽器不支持的情況下,zepto 還不提供這個方法。

$.trim

刪除字符串頭尾的空格

$.trim = function(str) { return str == null ? "" : String.prototype.trim.call(str) }

如果參數為 null 或者 undefined ,則直接返回空字符串,否則調用字符串原生的 trim 方法去除頭尾的空格。

$.type

類型檢測

$.type = type

$.type 調用的是內部方法 type ,該方法在前一篇文章《讀Zepto源碼之內部方法》中已有闡述。

能檢測的類型有 "Boolean Number String Function Array Date RegExp Object Error"

 

 

 

 

經過前面三章的鋪墊,這篇終於寫到了戲肉。在用 zepto 時,肯定離不開這個神奇的 $ 符號,這篇文章將會看看 zepto 是如何實現 $ 的。

讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zepto

源碼版本

本文閱讀的源碼為 zepto1.2.0

zepto的css選擇器 zepto.qsa

我們都知道,很多時候,我們都用$ 來獲取DOM對象,這跟 zepto.qsa 有很大的關系。

源碼

zepto.qsa = function(element, selector) { var found, // 已經找的到DOM maybeID = selector[0] == '#', // 是否為ID maybeClass = !maybeID && selector[0] == '.', // 是否為class nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // 將id或class前面的符號去掉 isSimple = simpleSelectorRE.test(nameOnly) // 是否為單個選擇器 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) ) }

以上是 qsa 的所有代碼,里面有用到一個正則表達式 simpleSelectorRE,先將這個正則消化下。

simpleSelectorRE = /^[\w-]*$/,

看到這個正則其實是匹配 a-z、A-Z、0-9、下划線、連詞符 組合起來的單詞,這其實就是單個 id 和 class 的命名規則。

從 return 中可以看出,qsa 其實是根據不同情況分別調用了原生的 getElementByIdgetElementsByClassNamegetElementsByTagName 和 querySelectorAll 的方法。

為什么要這么麻煩,不直接調用 querySelectorAll 方法呢?這是出於性能的考慮。這里有個簡單的測試。這個測試里,頁面上只有一個元素,如果比較復雜的時候,差距更加明顯。

好了,開始逐行分析代碼。

參數

  • element 開始查找的元素
  • selector 選擇器

變量

  • found: 已經找到的元素
  • maybeID = selector[0] == '#': 判斷選擇器的第一個字符是否為 #, 如果是 # ,則可能是 id 選擇器
  • maybeClass = !maybeID && selector[0] == '.' 如果不是 id 選擇器,並且選擇器的第一個字符為 . ,則可能是 class選擇器
  • nameOnly = maybeID || maybeClass ? selector.slice(1) : selector ,如果為 id 選擇器或者 class 選擇器,則將第一個字符去掉
  • isSimple = simpleSelectorRE.test(nameOnly) 是否為單選擇器,即 .single 的形式,不是 .first .secend 等形式

element.getElementById

(element.getElementById && isSimple && maybeID) 這是采用 element.getElementById 的條件。

首先要確保 element 具有 getElementById 的方法。getElementById 的方法是在 document 上的,Chrome等瀏覽器上,element 可能並不具有 geElementById 的方法,具體可以看看這篇文章:各瀏覽器對document.getElementById等方法的實現差異解析

然后要確保選擇器為單選擇器,並且為 id 選擇器。

返回值為 ((found = element.getElementById(nameOnly)) ? [found] : []), 如果能查找到元素,則將元素以數組的形式返回,否則返回空數組

排除不合法的element

element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11 。1 對應的是 Node.ELEMENT_NODE ,10 對應的是 Node.DOCUMENT_TYPE_NODE , 11 對應的是 Node.DOCUMENT_FRAGMENT_NODE ,如果不為以上三種類型,直接返回 []

終極三元表達式

slice.call( isSimple && !maybeID && element.getElementsByClassName ? // 如果為單選擇器並且不為id選擇器並且存在getElementsByClassName方法,進入下一個三元表達式判斷 maybeClass ? element.getElementsByClassName(nameOnly) : // 如果為class選擇器,則采用getElementsByClassName element.getElementsByTagName(selector) : // 否則采用getElementsByTagName方法 element.querySelectorAll(selector) // 以上情況都不是,則用querySelectorAll )

這里用了 slice.call 處理所獲取到的集合,這樣,獲取到的DOM集合就可以直接使用數組的方法了。

zepto.Z 函數

從第一篇代碼結構中我們已經知道,其實實現 $ 函數的核心是 zepto.init ,而 zepto.init 最終返回的是 zepto.Z 的結果。那就先來看看 zepto.Z

zepto.Z = function(dom, selector) { return new Z(dom, selector) }

zepto.Z 的代碼很簡單,返回的是 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 函數做的事情也很簡單,就是將 dom 數組轉化為類數組的形式,並設置對應的 length 屬性和 selector 屬性。

zepto.isZ

zepto.isZ = function(object) { return object instanceof zepto.Z }

既然看了 Z 函數,就順便也將 isZ 也一起看了吧。isZ 函數用來判斷參數 object 是否為 Z 的實例,這在 init 中會用到。

$的實現 zepto.init 函數

$的實現

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

可以看到,其實 $ 調用的就是 zepto.init 這個內部方法。

zepto.init

zepto.init = function(selector, context) { var dom // dom 集合 if (!selector) return zepto.Z() // 分支1 else if (typeof selector == 'string') { // 分支2 selector = selector.trim() if (selector[0] == '<' && fragmentRE.test(selector)) dom = zepto.fragment(selector, RegExp.$1, context), selector = null else if (context !== undefined) return $(context).find(selector) else dom = zepto.qsa(document, selector) } else if (isFunction(selector)) return $(document).ready(selector) // 分支3 else if (zepto.isZ(selector)) return selector // 分支4 else { // 分支5 if (isArray(selector)) dom = compact(selector) else if (isObject(selector)) dom = [selector], selector = null 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) } return zepto.Z(dom, selector) }

這個 init 方法代碼量不多,但是有大量的 if else, 希望我可以說得清楚

$的用法

$(selector, [context])   ⇒ collection  // 用法1 $(<Zepto collection>) ⇒ same collection // 用法2 $(<DOM nodes>) ⇒ collection // 用法3 $(htmlString) ⇒ collection // 用法4 $(htmlString, attributes) ⇒ collection v1.0+ // 用法5 Zepto(function($){ ... }) // 用法6

不傳參調用

直接調用 $() 時,對應的是分支1的情況: if (!selector) return zepto.Z() ,返回的是空的 Z 對象

selector 為 String 時

當 selector 為 string 時,對應的代碼在分支2,對應的用法是用法1、用法4和用法5

在這個分支里,又有三個子分支。一一來看一下:

第一個的判斷條件為 selector[0] == '<' && fragmentRE.test(selector) 。selector 的第一個字符為 < ,並且為html標簽 。fragmentRE 的定義如下 fragmentRE = /^\s*<(\w+|!)[^>]*>/ ,這個其實就是用來判斷字符串是否為標簽。 我對正則也不太熟,這里就不再展開。

如果滿足條件,則執行如下代碼:dom = zepto.fragment(selector, RegExp.$1, context), selector = null。 zepto.fragment其實是通過 htmlString 返回一個dom集合。這個函數稍后會說到,這里先不展開。這里對應的是用法4和用法5。

如果不滿足第一個判斷條件,則再判斷 context !== undefined (上下文是否存在)。如果存在,則查找 context 下選擇器為 selector 的所有子元素: $(context).find(selector) 。這個分支對應的是用法1

否則,調用 zepto.qsa 方法,查找 document 下的所有 selector : dom = zepto.qsa(document, selector)。這里對應的是用法1。

selector 為 Function 時

對應的代碼在分支3,對應的用法是用法6

這個分支很簡單,在頁面加載完畢后,再執行回調方法:$(document).ready(selector)

用過 zepto 的應該都熟悉這種用法: $(function() {})。其實走的就是這個分支

selector 為 Z 對象時

對應的代碼在分支4,對應的用法是用法2

如果參數已經為 Z 對象(zepto.isZ(selector)),則不需要做任何事情,直接原對象返回就可以了。

selector 為其他情況

如果為數組時(isArray(selector)), 將數組展平(dom = compact(selector))

如果為對象時(isObject(selector)),將對象包裹成數組(dom = [selector])。

以上兩種情況對應的是用法3,將dom對象或dom集合轉化為 z 對象

如果為標簽(fragmentRE.test(selector)),執行跟分支1一模一樣的代碼。這里判斷在上面已經做過了,為什么要再來一次呢?我也不太明白,有明白的可以跟我說下。

經過一輪又一輪的判斷和 selector 重置,現在終於可以調用 z 函數了: zepto.Z(dom, selector) ,init 的最后,將收集到的 dom 集合和對應的 selector 傳入 Z 函數,返回 Z 對象。

zepto.fragment

zepto.fragment = function(html, name, properties) { var dom, nodes, container if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)) if (!dom) { if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>") if (name === undefined) name = fragmentRE.test(html) && RegExp.$1 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) > -1) nodes[key](value) else nodes.attr(key, value) }) } return dom }

fragment 的作用的是將html片斷轉換成dom數組形式。

首先判斷是否為標簽的形式 singleTagRE.test(html) (如<div></div>), 如果是,則采用該標簽名來創建dom對象 dom = $(document.createElement(RegExp.$1)),不用再作其他處理。singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/

如果尚未獲取到 dom,接着進行:

if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")

這段是對 html 進行修復,如<p class="test" /> 修復成 <p class="test" /></p> 。正則表達式為 tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig

if (name === undefined) name = fragmentRE.test(html) && RegExp.$1

如果沒有指定標簽名,則獲取標簽名。如傳入 <div>test</div> ,獲取到的 name 為 div

if (!(name in containers)) name = '*' container = containers[name] container.innerHTML = '' + html dom = $.each(slice.call(container.childNodes), function() { container.removeChild(this) }) } // containers 已經開頭定義,如下 table = document.createElement('table'), tableRow = document.createElement('tr'), containers = { 'tr': document.createElement('tbody'), 'tbody': table, 'thead': table, 'tfoot': table, 'td': tableRow, 'th': tableRow, '*': document.createElement('div') }

檢測 name 是否為特殊的元素,如 tr 要用 tbody 包裹,其他的元素用 div 包裹。包裹元素的 childNodes 即為所需要獲取的 dom 。

if (isPlainObject(properties)) { nodes = $(dom) $.each(properties, function(key, value) { if (methodAttributes.indexOf(key) > -1) nodes[key](value) else nodes.attr(key, value) }) } // methodAttributes 在上面已經定義,定義如下 methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset']

如果屬性值為純對象,則給元素設置屬性。

如果所需設置的屬性,zepto已經定義了相應的方法,則調用zepto對應的方法,否則統一調用zepto的attr 方法設置屬性。

最后將 dom 返回

 

 

 

 

接下來幾個篇章,都會解讀 zepto 中的跟 dom 相關的方法,也即源碼 $.fn 對象中的方法。

讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zepto

.forEach()

forEach: emptyArray.forEach

因為 zepto 的 dom 集合是類數組,所以這里只是簡單地復制了數組的 forEach 方法。

具體的 forEach 的用法見文檔:Array.prototype.forEach()

.reduce()

reduce: emptyArray.reduce

簡單地復制了數組的 reduce 方法。

具體的 reduce 的用法見文檔:Array.prototype.reduce()

.push()

push: emptyArray.push

簡單地復制了數組的 push 方法。

具體的 push 的用法見文檔:Array.prototype.push()

.sort()

sort: emptyArray.sort

簡單地復制了數組的 sort 方法。

具體的 sort 的用法見文檔:Array.prototype.sort()

.splice()

splice: emptyArray.splice

簡單地復制了數組的 splice 方法。

具體的 splice 的用法見文檔:Array.prototype.splice()

.indexOf()

indexOf: emptyArray.indexOf

簡單地復制了數組的 indexOf 方法。

具體的 indexOf 的用法見文檔:Array.prototype.indexOf()

.get()

get: function(idx) { return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] },

這個方法用來獲取指定索引值的元素。

不傳參(idx === undefined)時,不傳參調用數組的 slice 方法,將集合中的所有元素返回。

當傳遞的參數大於或等於零(idx)時,返回相應索引值的元素 this[idx] ,如果為負數,則倒數返回this.[idx + this.length]

例如 $('li').get(-1) 返回的是倒數第1個元素,也即最后一個元素

.toArray()

toArray: function() { return this.get() }

toArray 方法是將元素的類數組變成純數組。toArray 內部不傳參調用 get 方法,上面已經分析了,當不傳參數時,get 方法調用的是數組方法 slice, 返回的自然就是純數組了。

.size()

size: function() { return this.length }

size 方法返回的是集合中的 length 屬性,也即集合中元素的個數。

.concat()

concat: function() { var i, value, args = [] for (i = 0; i < arguments.length; i++) { value = arguments[i] args[i] = zepto.isZ(value) ? value.toArray() : value } return concat.apply(zepto.isZ(this) ? this.toArray() : this, args) },

數組中也有對應的 concat 方法,為什么不能像上面的方法那樣直接調用呢?

這是因為 $.fn 其實是一個類數組對象,並不是真正的數組,如果直接調用 concat 會直接把整個 $.fn 當成數組的一個 item 合並到數組中。

for (i = 0; i < arguments.length; i++) { value = arguments[i] args[i] = zepto.isZ(value) ? value.toArray() : value }

這段是對每個參數進行判斷,如果參數是 zepto 的集合(zepto.isZ(value)),就先調用 toArray 方法,轉換成純數組。

return concat.apply(zepto.isZ(this) ? this.toArray() : this, args)

這段同樣對 this 進行了判斷,如果為 zepto 集合,也先轉換成數組。所以調用 concat 后返回的是純數組,不再是 zepto集合。

.map()

map: function(fn) { return $($.map(this, function(el, i) { return fn.call(el, i, el) })) }

map 方法的內部調用的是 zepto 的工具函數 $.map ,這在之前已經在《讀Zepto源碼之工具函數》做過了分析。

return fn.call(el, i, el)

map 方法對回調也做了包裝,call 的第一個參數為 el ,因此可以在 map 的回調中通過 this 來拿到每個元素。

map 方法對 $.map 返回的數組調用了 $() 方法,將返回的數組再次包裝成 zepto 對象,因此調用 map 方法后得到的數組,同樣具有 zepto 集合中的方法。

.slice()

slice: function() { return $(slice.apply(this, arguments)) }

slice 同樣沒有直接用數組的原生方法,也像 map 方法一樣,將返回的數組再次包裝成 zepto 對象。

.each()

each: function(callback) { emptyArray.every.call(this, function(el, idx) { return callback.call(el, idx, el) !== false }) return this },

zepto 的 each 方法比較巧妙,在方法內部,調用的其實是數組的 every 方法,every 遇到 false 時就會中止遍歷,zepto也正是利用 every 這種特性,讓 each 方法也具有了中止遍歷的能力,當 callback 返回的值為布爾值 false 時,中止遍歷,注意這里用了 !==,因為 callback 如果沒有返回值時,得到的值會是 undefined ,這種情況是需要排除的。

同樣,each 的回調中也是可以用 this 拿到每個元素的。

注意,each 方法最后返回的是 this, 所以在 each 調用完后,還可以繼續調用 集合中的其他方法,這就是 zepto 的鏈式調用,這個跟 map 方法中返回 zepto 集合的原理差不多,只不過 each 返回的是跟原來一樣的集合,map 方法返回的是映射后的集合。

.add()

add: function(selector, context) { return $(uniq(this.concat($(selector, context)))) }

add 可以傳遞兩個參數,selector 和 context ,即選擇器和上下文。

add 調用 $(selector, context) 來獲取符合條件的集合元素,這在上篇文章《讀Zepto源碼之神奇的$》已經有詳細的論述。

然后調用 concat 方法來合並兩個集合,用內部方法 uniq 來過濾掉重復的項,uniq 方法在《讀Zepto源碼之內部方法》已經有論述。最后也是返回一個 zepto 集合。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM