## 是否有必要講解整個源碼?
我個人覺得源碼還得靠自己來看,但是自己直接看源碼會有點暈頭轉向,因此本教程分析了整個代碼的設計思想,這樣再讀起來就方便了。
不過如果大部分同學覺得有必要把源碼再分析一遍,我可能會準備這樣一份教程給大家分享。
<br>
## 源碼注視
以下是我讀完了并寫了注釋了zepto core 模塊的源碼,如果您想繼續閱讀其他源碼,可參閱。
```js
/* Zepto v1.1.6 - zepto event ajax form ie - zeptojs.com/license */
var Zepto = (function() {
var undefined, key, $, classList,
// 獲取數組的slice 和 filter(返回數組中的滿足回調函數中指定的條件的元素)方法
emptyArray = [], slice = emptyArray.slice, filter = emptyArray.filter,
document = window.document,
elementDisplay = {}, classCache = {},
cssNumber = {
'column-count': 1,
'columns': 1,
'font-weight': 1,
'line-height': 1,
'opacity': 1,
'z-index': 1,
'zoom': 1
},
// 取出html代碼中第一個html標簽(或注釋),如取出 <p>123</p><h1>345</h1> 中的 <p>
fragmentRE = /^\s*<(\w+|!)[^>]*>/,
// 匹配 <img /> <p></p> 不匹配 <img src=""/> <p>123</p>
singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
// 單標簽
tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
// body html
rootNodeRE = /^(?:body|html)$/i,
// 大寫字母
capitalRE = /([A-Z])/g,
// special attributes that should be get/set via method calls
// 應該通過方法調用來設置/獲取的特殊屬性
methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'],
adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ],
table = document.createElement('table'),
tableRow = document.createElement('tr'),
// 指定特殊元素的 容器
containers = {
'tr': document.createElement('tbody'),
'tbody': table,
'thead': table,
'tfoot': table,
'td': tableRow,
'th': tableRow,
// 除了上面指定的,其他所有元素的容器都是 div
'*': document.createElement('div')
},
// interactive ???
readyRE = /complete|loaded|interactive/,
// 匹配一個包括(字母、數組、下劃線、-)的字符串
simpleSelectorRE = /^[\w-]*$/,
class2type = {},
toString = class2type.toString,
zepto = {},
camelize, uniq,
tempParent = document.createElement('div'),
// 屬性轉換為 camalCase 格式。
// $.fn.prop 方法用到了
propMap = {
'tabindex': 'tabIndex',
'readonly': 'readOnly',
'for': 'htmlFor',
'class': 'className',
'maxlength': 'maxLength',
'cellspacing': 'cellSpacing',
'cellpadding': 'cellPadding',
'rowspan': 'rowSpan',
'colspan': 'colSpan',
'usemap': 'useMap',
'frameborder': 'frameBorder',
'contenteditable': 'contentEditable'
},
// 判斷是否是arr的函數
isArray = Array.isArray || function(object){ return object instanceof Array }
// 上文定義 zepto = {}
// 判斷 element 是否符合 selector 的選擇要求
zepto.matches = function(element, selector) {
// selector有值,element有值,element是普通DOM節點
if (!selector || !element || element.nodeType !== 1) return false
// elem.matchesSelector('.item')
// 判斷當前的 elem 是否符合傳入的 selector 的要求
var matchesSelector = element.webkitMatchesSelector ||
element.mozMatchesSelector ||
element.oMatchesSelector ||
element.matchesSelector
if (matchesSelector) return matchesSelector.call(element, selector)
// 瀏覽器不支持 matchesSelector
// fall back to performing a selector:
var match,
parent = element.parentNode,
temp = !parent
// 上文定義 tempParent = document.createElement('div'),
// 如果沒有parent,parent賦值為一個div,然后將當前元素加入到這個div中
if (temp) {
parent = tempParent;
tempParent.appendChild(element);
// (parent = tempParent).appendChild(element); 這種寫法不易讀
}
// 通過 qsa 獲取匹配的元素,判斷其中有沒有 element
match = ~zepto.qsa(parent, selector).indexOf(element)
if (temp) {
// 如果沒有parent時,之前執行過 tempParent.appendChild(element);
// 此時要移除子元素
tempParent.removeChild(element);
}
// temp && tempParent.removeChild(element) // 這種寫法不易讀
// 返回最終的匹配結果,經過 qsa 判斷的結果
return match
}
function type(obj) {
return obj == null ?
String(obj) : // null undefined
class2type[toString.call(obj)] || "object"
// 下文定義:
// // Populate the class2type map
// $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
// class2type[ "[object " + name + "]" ] = name.toLowerCase()
// })
}
function isFunction(value) { return type(value) == "function" }
// window的特點:window.window === window
function isWindow(obj) { return obj != null && obj == obj.window }
// document.nodeType === 9
// elem.DOCUMENT_NODE 也等于 9 (這里直接判斷是不是9也行???)
function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }
function isObject(obj) { return type(obj) == "object" }
// 判斷是否是最基本的object:Object.getPrototypeOf(obj) == Object.prototype
function isPlainObject(obj) {
return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
}
// 數組或者對象數組
function likeArray(obj) { return typeof obj.length == 'number' }
// 篩選數組,踢出 null undefined 元素
function compact(array) { return filter.call(array, function(item){ return item != null }) }
// 下文定義:
// $.fn = {
// concat: emptyArray.concat,
// $.fn.concat.apply([], array) —— 無論 array 是不是數組,都將返回一個數組,
// 例如 $.fn.concat.call([], 'abc') 返回的是 ['abc']
function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array }
// camelize 已在上文定義
// 用于 css 的 camalCase 轉換,例如 background-color 轉換為 backgroundColor
camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) }
// 將 lineHeight 轉換為 line-height 格式
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()
}
// uniq變量已經在前面定義
// 用來將 [1,1,2,2,3,3] 替換為 [1,2,3]
uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) }
// 上文定義 classCache = {}
function classRE(name) {
return name in classCache ?
classCache[name] :
(classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
// classCache 存儲的數據是這樣的:
// {
// abc: /(^|\s)abc(\s|$)/, // 能匹配 'abc' 或 ' abc ' 或 ' abc' 或 'abc '
// xyz: /(^|\s)abc(\s|$)/,
// ...
// }
}
// 傳入一個 css 的 name 和 value,判斷這個 value 是否需要增加 'px'
function maybeAddPx(name, value) {
// dasherize(name) 將 lineHeight 轉換為 line-height 格式
// !cssNumber[dasherize(name)] 判斷轉換出來的 css name 是否再這個數組之外
return (typeof value == "number" && !cssNumber[dasherize(name)]) ?
// 如果 value 是數字,并且 name 不在 cssNumber 數組之內,就需要加 'px',否則不需要
// 例如 'width'、'font-size' 就需要加 'px', 'font-weight' 就不需要加
value + "px" :
value
// 前文定義----------------------
// cssNumber = {
// 'column-count': 1,
// 'columns': 1,
// 'font-weight': 1,
// 'line-height': 1,
// 'opacity': 1,
// 'z-index': 1,
// 'zoom': 1
// },
// 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()
// }
}
// 獲取一個元素的默認 display 樣式值,可能的結果是:inline block inline-block table .... (none 轉換為 block)
// $.fn.show 方法中用到了
function defaultDisplay(nodeName) {
var element, display
// 前文定義 elementDisplay = {}
if (!elementDisplay[nodeName]) {
// 如果 elementDisplay 對象中,沒有存儲 nodeName 的信息
// 則新建一個 nodeName 元素,添加到 body 中
element = document.createElement(nodeName)
document.body.appendChild(element)
// 獲取它的默認的 display 樣式信息。
display = getComputedStyle(element, '').getPropertyValue("display")
// 接著馬上移除元素!!!
element.parentNode.removeChild(element)
// 'none' 換成 'block',另外還可能是 'inline' 'inline-block' 'table' 等等...
display == "none" && (display = "block")
// 存儲下來
elementDisplay[nodeName] = display
// 下文定義
// var nativeGetComputedStyle = getComputedStyle;
// window.getComputedStyle = function(element){
// try {
// return nativeGetComputedStyle(element)
// } catch(e) {
// return null
// }
// }
// 解釋:
// 如果瀏覽器支持 getComputedStyle 則使用,如果不支持,就返回 null
// getComputedStyle(elem, '偽類,如 :link') 返回一個 CSSStyleDeclaration 對象,里面存儲了元素的樣式信息,可以通過 getPropertyValue('name') 方法獲取
}
// 最終返回 display 結果
return elementDisplay[nodeName]
}
// 返回一個元素的子元素,數組形式
function children(element) {
// 有些瀏覽器支持 elem.children 獲取子元素,有些不支持
return 'children' in element ?
// 上文定義 slice = [].slice
// slice.call(likeArr) 可以將對象數組轉換為真正的數組
slice.call(element.children) :
// 瀏覽器不支持 elem.children 只能通過 elem.childNodes 獲取子元素
// 只去 node.nodeType == 1 的子元素,通過 $.map 拼接成數組
// $.map 下文定義的, $.map = function (elements, callback) {....}
// $.map 作用:針對 elements(對象數組或數組),對每個元素都經過 callback 函數的過濾,并將過濾通過的元素,push到一個新數組中,返回新數組
$.map(element.childNodes, function(node){ if (node.nodeType == 1) return node })
}
// 上文定義 zepto = {}
// 上文定義 zepto.matches = function(element, selector) { /* 判斷elem是否符合selector的要求 */ }
// `$.zepto.fragment` takes a html string and an optional tag name
// to generate DOM nodes nodes from the given html string.
// The generated DOM nodes are returned as an array.
// This function can be overriden in plugins for example to make
// it compatible with browsers that don't support the DOM fully.
zepto.fragment = function(html, name, properties) {
/*
參數:
@html: 待處理的html字符串
@name: 通過 name 可在 containers 中查找容器節點,如果不傳入,取得的容器默認為 div
@properties: 節點屬性對象
*/
var dom, nodes, container
// 上文定義:singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, // 匹配 <img /> <p></p> 不匹配 <img src=""/> <p>123</p>
// 如果 html 是單標簽,則直接用該標簽創建元素
// RegExp.$1 表示正則中的第一個括號匹配的內容,在此即 (\w+) 匹配的內容,
// A special case optimization for a single tag
if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
if (!dom) {
// 說明 html 不是單標簽,dom未被賦值
// 上文定義 tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, // 單標簽
// 將 <p/>或<p />,替換為 <p></p>,將<p abc/>替換為<p>abc</p>
// <input/> (在 tagExpanderRE 中定義)的不替換
if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
// fragmentRE = /^\s*<(\w+|!)[^>]*>/, // 取出html代碼中第一個html標簽(或注釋),如取出 <p>123</p><h1>345</h1> 中的 <p>
// 如果 name 未傳入,則賦值為 html 的第一個標簽
if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
// 上文定義
// // 指定特殊元素的 容器
// containers = {
// 'tr': document.createElement('tbody'),
// 'tbody': table,
// 'thead': table,
// 'tfoot': table,
// 'td': tableRow,
// 'th': tableRow,
// // 除了上面指定的,其他所有元素的容器都是 div
// '*': document.createElement('div')
// },
if (!(name in containers)) name = '*'
container = containers[name]
container.innerHTML = '' + html // 轉變為字符串的快捷方式
// 遍歷 container 的子元素(先轉換為數組形式)
// 返回的同時,將每個子元素移除。
// $.each 返回的是一個數組,因為第一個參數就是數組 slice.call(container.childNodes)
dom = $.each(slice.call(container.childNodes), function(){
container.removeChild(this)
})
}
// 賦值屬性
if (isPlainObject(properties)) {
// 先將dom轉換為 zepto 對象
nodes = $(dom)
$.each(properties, function(key, value) {
// 上文定義:
// // 應該通過方法調用來設置/獲取的特殊屬性
// methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'],
if (methodAttributes.indexOf(key) > -1) nodes[key](value) // 滿足 methodAttributes 的,通過方法賦值
else nodes.attr(key, value) // 否則,通過屬性復制
})
}
// 最終返回的dom可能有兩種形式
// 第一,如果 html 是單標簽,則dom被復制為一個zepto對象 dom = $(document.createElement(RegExp.$1))
// 第二,如果 html 不是單標簽,則dom被復制為一個DOM節點的數組
return dom
}
// 上文定義 zepto = {}
// 上文定義 zepto.matches = function(element, selector) { /* 判斷elem是否符合selector的要求 */ }
// 上文定義 zepto.fragment = function(html, name, properties) { /* 通過html字符串獲取文檔碎片 */ }
// `$.zepto.Z` swaps out the prototype of the given `dom` array
// of nodes with `$.fn` and thus supplying all the Zepto functions
// to the array. Note that `__proto__` is not supported on Internet
// Explorer. This method can be overriden in plugins.
zepto.Z = function(dom, selector) {
dom = dom || []
// 將 dom 隱式原型強制改為 $.fn
// 下文 zepto.Z.prototype = $.fn 因此,dom.__proto__ = $.fn 即 dom.__proto__ = zepto.Z.prototype 可以不較真的認為 zepto.Z 就是一個構造函數(但感覺這么設計,有些蹩腳)
dom.__proto__ = $.fn
dom.selector = selector || ''
return dom
}
// `$.zepto.isZ` should return `true` if the given object is a Zepto
// collection. This method can be overriden in plugins.
zepto.isZ = function(object) {
// 上文 dom.__proto__ = $.fn
// 下文 zepto.Z.prototype = $.fn
// 可知:dom.__proto__ === $.fn === zepto.Z.prototype
// 因此,zepto對象都符合 object instanceof zepto.Z
return object instanceof zepto.Z
}
// `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and
// takes a CSS selector and an optional context (and handles various
// special cases).
// This method can be overriden in plugins.
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') {
// 字符串的情況,一般有兩種:
// 第一,一段 html 代碼,旨在通過zepto生成dom對象
// 第二,一段查詢字符串,旨在通過zepto查找dom對象
// 將查詢結果存儲到 dom 變量中
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 <
// 上文定義:
// // 取出html代碼中第一個html標簽(或注釋),如取出 <p>123</p><h1>345</h1> 中的 <p>
// fragmentRE = /^\s*<(\w+|!)[^>]*>/,
if (selector[0] == '<' && fragmentRE.test(selector))
// 第一,RegExp.$1取出來的就是第一個標簽名稱,即正則中 (\w+|!) 對應的內容
// 第二,此時的 context 應該傳入的是css屬性對象(這里會產生歧義,老版的不會傳入 context)
dom = zepto.fragment(selector, RegExp.$1, context), selector = null
// If there's a context, create a collection on that context first, and select
// nodes from there
// 如果 selector 不是html字符串標簽,并且 context 有值,則從context中查找
// find 應該是在 $.fn 中定義的,有待解讀???
else if (context !== undefined) return $(context).find(selector)
// If it's a CSS selector, use it to select nodes.
// 除了以上情況,就從整個 document 執行 qsa 的查找
else dom = zepto.qsa(document, selector)
}
// If a function is given, call it when the DOM is ready
// 如果是函數,則dom ready時執行,
// ready方法應該在 $.fn 中定義,有待解毒
else if (isFunction(selector)) return $(document).ready(selector)
// If a Zepto collection is given, just return it
// 傳入的參數本身就已經是 zepto 對象,則直接返回
else if (zepto.isZ(selector)) return selector
else {
// compact函數:踢出數組中 == null 的元素
// normalize array if an array of nodes is given
if (isArray(selector)) dom = compact(selector)
// 如果傳入的是object,直接強制塞進一個數組
// Wrap DOM nodes.
else if (isObject(selector)) dom = [selector], selector = null // 及時清空 selector 不妨礙下面的判斷
// 從此往下,感覺和上文 selector 是字符串的情況下,重復了
// ????????
// fragmentRE.test 即判斷字符串是否是 html 標簽開頭(即是否是html fragement)
// If it's a html fragment, create nodes from it
else if (fragmentRE.test(selector))
//此時,context 也是屬性集合,不是容器!!!
//(這里會產生歧義,老版的不會傳入 context)
dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null // 及時清空 selector 不妨礙下面的判斷
// 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)
}
// 最終,還是通過 zepto.Z 創建了對象
// 這里的 dom,其實就是一個數組
// create a new Zepto collection from the nodes found
return zepto.Z(dom, selector)
}
// `$` will be the base `Zepto` object. When calling this
// function just call `$.zepto.init, which makes the implementation
// details of selecting nodes and creating Zepto collections
// patchable in plugins.
$ = function(selector, context){
return zepto.init(selector, context)
}
// $ 最終被這個匿名函數所返回,并復制給了全局的 Zepto 變量
// 全局的 zepto 變量暴露給了 window,并且可能有一個別名—— $
// 此 $ 非彼 $
// 對于初學者來說,這里肯定非常繞(還不如把這里的 $ 改改名字)
function extend(target, source, deep) {
// key 在上文已經定義,否則就污染全局變量了
for (key in source)
// 深度遞歸,首先必須 deep 參數為 true
// 其次,source[key] 必須是數組或者對象,才有必要深度遞歸(否則沒必要)
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
// source[key] 是對象,而 target[key] 不是對象
// 則 target[key] = {} 初始化一下,否則遞歸會出錯的
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}
// source[key] 是數組,而 target[key] 不是數組
// 則 target[key] = [] 初始化一下,否則遞歸會出錯的
if (isArray(source[key]) && !isArray(target[key]))
target[key] = []
// 執行遞歸
extend(target[key], source[key], deep)
}
// 不滿足以上條件,說明 source[key] 是一般的值類型,直接賦值給 target 就是了
else if (source[key] !== undefined) target[key] = source[key]
}
// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target){
// 一般傳入的參數會是:
// (targetObj, srcObj1, srcObj2, srcObj1...)
// (true, targetObj, srcObj1, srcObj2, srcObj1...)
// arguments 是對象數組,slice.call 會返回真正的數組(此處返回從第二項開始)
var deep, args = slice.call(arguments, 1)
// 第一個參數是boolean,這里會把第二個參數當做 target,其他的作為 source
if (typeof target == 'boolean') {
deep = target
target = args.shift()
}
// 將所有的 source 添加到 target 中
args.forEach(function(arg){ extend(target, arg, deep) })
return target
// 感覺這樣設計是比較好,很好的將業務和底層進行了分離(雖然比較簡單):
// 核心方法再 function extend(...){...} 中定義,
// 而 $.extend 方法中做一些外圍的判斷和處理,最終調用 extend 函數去執行
}
// `$.zepto.qsa` is Zepto's CSS selector implementation which
// uses `document.querySelectorAll` and optimizes for some special cases, like `#id`.
// This method can be overriden in plugins.
zepto.qsa = function(element, selector){
/*
@element: 容器
@selector: 選擇器
*/
var found,
maybeID = selector[0] == '#',
maybeClass = !maybeID && selector[0] == '.',
// ID或class形式:返回 selector.slice(1) 即ID或者class的值
// 否則:返回 selector,如通過 tagName 查詢
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
// 是否是一個簡單的字符串(可能是一個復雜的選擇器,如 'div#div1 .item[link] .red')
isSimple = simpleSelectorRE.test(nameOnly)
// 上文定義:
// // 匹配一個包括(字母、數組、下劃線、-)的字符串
// simpleSelectorRE = /^[\w-]*$/,
// 以下代碼的基本思路是:
// 1. 優先通過 ID 獲取元素;
// 2. 然后試圖通過 className 和 tagName 獲取元素
// 3. 最后通過 querySelectorAll 來獲取
return (isDocument(element) && isSimple && maybeID) ?
// 這是最簡單的形式:容器是document、選擇器是一個id
// 因為 getElementById 只能在 document 上用,所以這里單獨拿出來
( (found = element.getElementById(nameOnly)) ?
[found] :
[]
) :
(element.nodeType !== 1 && element.nodeType !== 9) ?
// 容器不是一般元素,也不是document,直接返回 []
[] :
// 將獲取的所有元素集合,都轉換為數組
slice.call(
isSimple && !maybeID
// isSimple情況下,nameOnly 只可能是 className 或者 tagName
// getElementsByClassName 和 getElementsByTagName 可以在 elem 上用,而且比 querySelectorAll 速度快
// 所以,只要elem容器有值,盡量單獨拿出來處理
?
maybeClass ?
element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
element.getElementsByTagName(selector) // Or a tag
// 最后其他情況,只能通過 querySelectorAll 來處理
:
element.querySelectorAll(selector) // Or it's not simple, and we need to query all
)
}
// 根據 selector 篩選 nodes
// 并將 nodes 封裝為 zepto 對象
// $.fn.filter 下文定義
function filtered(nodes, selector) {
return selector == null ? $(nodes) : $(nodes).filter(selector)
}
// 判斷 parent 是否包含 node
$.contains = document.documentElement.contains ?
// 瀏覽器支持 contains 方法
function(parent, node) {
return parent !== node && parent.contains(node)
} :
// 不支持 contains 方法
function(parent, node) {
while (node && (node = node.parentNode))
if (node === parent) return true
return false
}
// 如果 arg 是函數,則改變函數的執行環境和參數
// 如果不是,直接返回 arg
// $.fn.html 方法就用到了
function funcArg(context, arg, idx, payload) {
return isFunction(arg) ? arg.call(context, idx, payload) : arg
}
// 設置屬性
function setAttribute(node, name, value) {
value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
}
// 設置或獲取 node 的 className
// 考慮 svg ????
// access className property while respecting SVGAnimatedString
function className(node, value){
var klass = node.className || '',
svg = klass && klass.baseVal !== undefined
// 獲取
if (value === undefined) return svg ? klass.baseVal : klass
// 設置
svg ? (klass.baseVal = value) : (node.className = value)
}
// 將字符串變成響應的對象或者值,例如源代碼的注釋:
// "true" => true
// "false" => false
// "null" => null
// "42" => 42
// "42.5" => 42.5
// "08" => "08"
// JSON => parse if valid
// String => self
function deserializeValue(value) {
try {
return value ?
// value『有值』的情況:
value == "true" || // 如果 value == 'true',那么這個表達式本身就返回 true ,導致整個函數返回true
// value !== 'true' 的情況:
(
value == "false" ? false : // "null" => null
value == "null" ? null : // "null" => null
+value + "" == value ? +value : // 數字:"42" => 42 "42.5" => 42.5 ( 但是 '08' 卻不符合這個條件 )
/^[\[\{]/.test(value) ? $.parseJSON(value) : // '[...]' 或者 '{...}'
value // 其他
)
// value『無值』的情況: undefined / '' / flase / 0 / null
: value
} catch(e) {
return value
}
}
// 將上文定義的函數,暴露給 $ 對象(其實 $ 是一個 function)
$.type = type
$.isFunction = isFunction
$.isWindow = isWindow
$.isArray = isArray
$.isPlainObject = isPlainObject
$.isEmptyObject = function(obj) {
var name
for (name in obj) return false
return true
}
$.inArray = function(elem, array, i){
return emptyArray.indexOf.call(array, elem, i)
}
$.camelCase = camelize
$.trim = function(str) {
return str == null ? "" : String.prototype.trim.call(str)
}
// plugin compatibility
$.uuid = 0
$.support = { }
$.expr = { }
// 重新組織 elements 對象(數組、對象或者對象數組),針對每一個元素,都用 callback 進行檢驗
// 檢驗通過后,將元素push進一個新數組,并返回
$.map = function(elements, callback){
var value, values = [], i, key
// 數組,或者對象數組
if (likeArray(elements))
for (i = 0; i < elements.length; i++) {
// 遍歷,經過 callback 驗證,push到結果中
value = callback(elements[i], i)
if (value != null) values.push(value)
}
// 對象
else
for (key in elements) {
// 遍歷,經過 callback 驗證,push到結果中
value = callback(elements[key], key)
if (value != null) values.push(value)
}
// 返回數組
// flatten 函數上文定義的,作用:無論 values 是否是數組,都將返回一個正確的數組。例如,傳入 'abc' ,返回 ['abc']
return flatten(values)
}
// 遍歷 elements 所有元素(數組、對象數組、對象),執行 callback 方法,最終還是返回 elements
// 注意1:callback.call(elements[i], i, elements[i]) 函數執行的環境和參數
// 注意2:=== false) return elements 一旦有函數返回 false,即跳出循環,類似 break
// 注意3:無論哪種情況,最終返回的還是 elements
$.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
}
// 上文定義:filter = emptyArray.filter
// 篩選數組
$.grep = function(elements, callback){
return filter.call(elements, callback)
}
if (window.JSON) $.parseJSON = JSON.parse
// Populate the class2type map
$.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase()
/*
上文將 class2type 賦值為 {}
最終將 class2type 賦值為:
{
'[object boolean]': 'boolean',
'[object number]': 'number',
'[object string]': 'string',
...
}
存儲這個數據是為了方便的獲取一些對象的類型,
例如 Object.prototype.toString.call([]) 返回的是 '[Object Array]'
那么即可根據這個獲取 [] 的類型是 'array'
*/
})
// Define methods that will be available on all
// Zepto collections
$.fn = {
// 為何要這么多數組的方法?
// 因為一個 zepto 對象,本身就是一個數組
// Because a collection acts like an array
// copy over these useful array functions.
forEach: emptyArray.forEach,
reduce: emptyArray.reduce, // 方法何用????
push: emptyArray.push,
sort: emptyArray.sort,
indexOf: emptyArray.indexOf,
concat: emptyArray.concat,
// `map` and `slice` in the jQuery API work differently
// from their array counterparts
map: function(fn){
// $.map 上文定義的, $.map = function (elements, callback) {....}
// $.map 作用:針對 elements(對象、對象數組或數組),對每個元素都經過 callback 函數的過濾,并將過濾通過的元素,push到一個新數組中,返回新數組
// 最后,用 $ 封裝返回
return $(
// $.map 返回的是一個數組
$.map(this,
// 針對每一個元素,都執行傳入的函數,如果函數返回的 !=null 就將插入到新返回的數組
function(el, i){ return fn.call(el, i, el) }
)
)
/*
$('div').map(function(key, value){
return value.id;
// 或者 return this.id;
})
這個結果就是 $(['div1', 'div2' ...])
*/
},
slice: function(){
// 直接數組的slice方法,并將結果用 $ 封裝返回
return $(slice.apply(this, arguments))
},
// 在 zepto.init 函數中,當傳入的函數是函數時,就用到了 ready
// else if (isFunction(selector)) return $(document).ready(selector)
ready: function(callback){
// need to check if document.body exists for IE as that browser reports
// document ready when it hasn't yet created the body element
// 下文定義:readyRE = /complete|loaded|interactive/,
if (readyRE.test(document.readyState) && document.body) callback($)
else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false)
// 返回當前對象
return this
},
get: function(idx){
return idx === undefined ?
slice.call(this) : // 未傳參數,直接返回一整個數組
// 有參數,則試圖返回單個元素(大于0,小于0 兩種情況)
this[
idx >= 0 ?
idx :
idx + this.length
]
},
// 將zepto集合變為純數組
toArray: function(){ return this.get() },
size: function(){
return this.length
},
// 將元素從這個DOM樹中移除
remove: function(){
return this.each(function(){
if (this.parentNode != null)
this.parentNode.removeChild(this)
})
},
each: function(callback){
// [].every ES5中Array的新特性。循環數組每個元素,返回是否符合callback函數的要求
// every 函數返回的是 false 或者 true(不過這里返回什么無所謂,執行就可以了)
emptyArray.every.call(this, function(el, idx){
return callback.call(el, idx, el) !== false
})
// 最后返回本身對象
return this
},
filter: function(selector){
// not函數下文定義
// 如果給not傳入的參數是函數,則返回不符合這個函數規則的元素的數組(用 $ 封裝)
if (isFunction(selector)) return this.not(this.not(selector))
// 上文定義:zepto.matches 判斷elements是否符合 selector 的要求
// zepto.matches = function(element, selector) {...}
return $(filter.call(this, function(element){
// 利用 [].filter 方法做篩選,利用 zepto.matches 做判斷
return zepto.matches(element, selector)
}))
},
// $('div') 可能只有三個 div 節點,那么 $('div').add('p') 再三個 div 節點的基礎上,增加三個 p 節點
add: function(selector,context){
// uniq函數——數組去重,例如:用來將 [1,1,2,2,3,3] 替換為 [1,2,3]
return $(uniq(this.concat($(selector,context))))
},
is: function(selector){
// 注意:這里只對 this[0] 第一個元素做判斷了,其他的元素不管了
return this.length > 0 && zepto.matches(this[0], selector)
},
not: function(selector){
var nodes=[] // 存儲最后返回的結果
// 如果參數是函數
if (isFunction(selector) && selector.call !== undefined)
this.each(function(idx){
// 遍歷對象的所有元素,對每個元素都執行傳入的函數
// 當函數返回 false 時(即不符合函數的規則),則將當前元素push到結果中,等待返回
if (!selector.call(this,idx)) nodes.push(this)
})
// 如果參數不是函數
else {
// 為 excludes 賦值
var excludes =
// 如果 selector 是字符串(css選擇器),則用filter過濾,將結果存儲到 excludes 中
typeof selector == 'string' ? this.filter(selector) :
// 如果 selector 不是字符串
// 如果是數組或者對象數組(并且 selector.item 是函數???),則生成數組,賦值給 excludes
(likeArray(selector) && isFunction(selector.item)) ? slice.call(selector)
// 否則直接生成 zepto 對象,賦值給 excludes
: $(selector)
// 至此,excludes 中就存儲了通過 selector 查找出來的元素
// [].forEach 是ES5的新特性
this.forEach(function(el){
// 取出 excludes 中不包含的元素,push到結果中
if (excludes.indexOf(el) < 0) nodes.push(el)
})
}
// 返回最后的結果,用 $ 封裝
return $(nodes)
},
has: function(selector){
// 經過 filter 函數處理,返回的是一個處理后的值
return this.filter(function(){
return isObject(selector) ?
// 如果 seletor 是 object(可能是elem節點),則用 $.contains 判斷
$.contains(this, selector) :
// 否則(selector是css選擇字符串)則返回find后的size(如果 size === 0 即相當于返回 false)
$(this).find(selector).size()
// $.fn.find 在下文定義
})
},
eq: function(idx){
// 取出指定index的元素
// 可支持 -1、0、1、2 ……
return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1)
},
first: function(){
var el = this[0]
// 不是 object 則直接返回
// 是 object 類型,則用 $ 封裝 (因為時刻都要支持鏈式操作!!!)
return el && !isObject(el) ? el : $(el)
},
last: function(){
var el = this[this.length - 1]
return el && !isObject(el) ? el : $(el)
},
find: function(selector){
// result 存儲返回結果
var result, $this = this
// 如果沒有參數,就返回一個空的 zepto 對象
if (!selector) result = $()
// 如果selector是對象
else if (typeof selector == 'object')
result = $(selector).filter(function(){
var node = this
return emptyArray.some.call($this, function(parent){
return $.contains(parent, node)
})
})
// 如果 selector 不是對象(即是css選擇器):
// 如果只有一個元素,則使用 qsa 判斷,結果經過 $ 封裝后賦值給 result
else if (this.length == 1) result = $(zepto.qsa(this[0], selector))
// 如果有多個元素,則使用 map 遍歷所有元素,使用 qsa 針對每個元素判斷,符合條件即返回(map將返回包含符合條件的元素的新數組,并 $ 封裝,支持鏈式操作!!)
else result = this.map(function(){ return zepto.qsa(this, selector) })
// 返回最終結果
return result
},
// 從元素本身開始,逐級向上級元素匹配,并返回最先匹配selector的元素
closest: function(selector, context){
var node = this[0], collection = false
// 如果 selector 是對象,則用 $ 封裝后,賦值給 collection
if (typeof selector == 'object') collection = $(selector)
while (
// while循環的判斷條件:
// 第一,node有值(node一開始被賦值為對象的第一個元素)
// 第二,collection有值(傳入的selector是對象)則collection包含node;collection無值(傳入的selector是字符串,css選擇),則node滿足selector條件
// 滿足第一個條件,不滿足第二條件,則循環繼續(node試圖賦值為node.parentNode);否則,循環跳出(說明已經找到了符合條件的父節點)
node && !(
collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector)
)
)
// node賦值成 node.parentNode
// 前提條件是:node != context && node 不是 document,如果是這兩個條件之一,那就不繼續賦值
node = node !== context && !isDocument(node) && node.parentNode
// 返回最終結果
return $(node)
},
// 獲取對象集合每個元素所有的祖先元素。 $('h1').parents() => [<div#container>, <body>, <html>]
parents: function(selector){
var ancestors = [], nodes = this
while (nodes.length > 0)
// 可能需要執行多次 while 循環
// 每次執行 $.map 函數都會對 nodes 重新賦值,然后再判斷是否需要繼續循環
// 因為要獲取每個元素的所有祖先元素,所以要多次循環
nodes = $.map(nodes, function(node){
// 使用 $.map(返回符合條件的元素的新數組,并用 $ 封裝)遍歷所有元素
if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) {
// 將符合條件的元素push到結果中
// 條件:不能是 document,結果中元素不能重復。否則不執行push
ancestors.push(node)
// 返回的 node ,將拼接出新數組,重新復制給 nodes,然后試圖繼續執行 while 循環
return node
}
})
// 如果css選擇器參數給出,過濾出符合條件的元素
return filtered(ancestors, selector)
},
// 獲取對象集合中每個元素的直接父元素。如果css選擇器參數給出。過濾出符合條件的元素。
parent: function(selector){
// pluck 函數在下文定義
// parent 函數,只獲取第一級父節點即可
return filtered(uniq(this.pluck('parentNode')), selector)
},
// 獲得每個匹配元素集合元素的直接子元素,可通過 selector 過濾
children: function(selector){
return filtered(
this.map(function(){ return children(this) }), selector
)
},
// 獲得每個匹配元素集合元素的子元素,包括文字和注釋節點
contents: function() {
return this.map(function() { return slice.call(this.childNodes) })
},
// 獲取對象集合中所有元素的兄弟節點,可通過 selector 過濾
siblings: function(selector){
return filtered(this.map(function(i, el){
// 獲取兄弟節點
return filter.call(children(el.parentNode), function(child){ return child!==el })
}), selector)
},
empty: function(){
return this.each(function(){ this.innerHTML = '' })
},
// `pluck` is borrowed from Prototype.js
pluck: function(property){
// 獲取自定義屬性,返回值,拼接數組
return $.map(this, function(el){ return el[property] })
},
show: function(){
// 返回當前對象,保證可鏈式操作
return this.each(function(){
// 第一步,針對內聯樣式,將 none 改為空字符串,如 <p id="p2" style="display:none;">p2</p>
this.style.display == "none" && (this.style.display = '')
// 第二步,針對css樣式,如果是 none 則修改為默認的顯示樣式
if (getComputedStyle(this, '').getPropertyValue("display") == "none")
this.style.display = defaultDisplay(this.nodeName)
// show 方法是為了顯示對象,而對象隱藏的方式有兩種:內聯樣式 或 css樣式
// this.style.display 只能獲取內聯樣式的值(獲取屬性值)
// getComputedStyle(this, '').getPropertyValue("display") 可以獲取內聯、css樣式的值(獲取 renderTree 的值)
// 因此,這兩步都要做判斷,
})
},
replaceWith: function(newContent){
// 先在前面插入,然后將當前對象移除
return this.before(newContent).remove()
},
wrap: function(structure){
// 是否是函數
var func = isFunction(structure)
if (this[0] && !func)
var dom = $(structure).get(0),
// 何時用 clone ?
// 第一,dom.parentNode 說明 dom 在文檔結構中,不 clone 就會被移動
// 第二,this.length > 1 說明當前對象有多個元素,每個元素都要添加,所有要clone
clone = dom.parentNode || this.length > 1
return this.each(function(index){
// 借用 wrapAll 方法來做包裝
$(this).wrapAll(
func ? structure.call(this, index) :
clone ? dom.cloneNode(true) : dom
)
})
},
// 在所有匹配元素外面包一個單獨的結構
wrapAll: function(structure){
if (this[0]) {
// 先將 structure 插入到文檔結構
$(this[0]).before(structure = $(structure))
var children
// drill down to the inmost element
// 通過循環,將 structure 重新賦值為當前 structure 的最深處的一個子元素
while ((children = structure.children()).length) structure = children.first()
// 將所有子元素都包裹進 structure
$(structure).append(this)
}
// 返回當前對象
return this
},
wrapInner: function(structure){
// 是否是函數
var func = isFunction(structure)
// 返回對象自身,保證鏈式操作
return this.each(function(index){
var self = $(this), contents = self.contents(),
// 是函數,即獲取函數執行的返回結果;否則直接用 structure 參數
dom = func ? structure.call(this, index) : structure
// 如果當前元素有內容,則通過內容 wrapAll。無內容,則直接用自身的 append 增加
contents.length ? contents.wrapAll(dom) : self.append(dom)
})
},
unwrap: function(){
// 通過 this.parent() 獲取每個元素的父節點(集合)
// 遍歷這個父節點的集合
this.parent().each(function(){
// 將當前父節點替換為它的子節點
$(this).replaceWith($(this).children())
})
// 返回對象自身,保證鏈式操作
return this
},
clone: function(){
// 通過 this.map 循環對象,
// 針對每個元素都返回它的clone 這個 this 和前面的 this 不一樣
// 返回新數組(用 $ 封裝)
return this.map(function(){ return this.cloneNode(true) })
},
hide: function(){
return this.css("display", "none")
},
// 切換顯示和隱藏
toggle: function(setting){
/*
@setting
true : 強制切換為 show
false : 強制切換為 hide
*/
return this.each(function(){
var el = $(this)
// 條件判斷:
// 如果 setting === undefined 則看 el.css("display") == "none"
// 如果 setting !== undefined 則看 !!setting
;(setting === undefined ? el.css("display") == "none" : setting) ?
el.show() : // 如果 true 則顯示
el.hide() // 如果 false 則隱藏
})
},
// 借助 previousElementSibling 屬性
prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') },
// 借助 nextElementSibling 屬性
next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') },
html: function(html){
return
// 情況1:有參數,賦值,并返回自身
0 in arguments ?
this.each(function(idx){
var originHtml = this.innerHTML
// 傳入的 html 參數允許是一個字符串,也允許是一個函數
// 通過 funcArg 函數:
// 1.如果 html 是字符串,則返回html
// 2.如果 html 是函數,則執行執行函數(傳入 idx、originHtml),返回函數執行結果
$(this).empty().append( funcArg(this, html, idx, originHtml) )
}) :
/*
插播:
function funcArg(context, arg, idx, payload) {
return isFunction(arg) ? arg.call(context, idx, payload) : arg
}
*/
// 情況2:無參數,取值
(0 in this ?
this[0].innerHTML : // 直接取第一個元素的 innerHTML
null
)
},
text: function(text){
return
// 情況1:有參數,賦值,并返回自身
0 in arguments ?
this.each(function(idx){
// funcArg的應用,和html方法中一樣
var newText = funcArg(this, text, idx, this.textContent)
this.textContent = newText == null ? '' : ''+newText
}) :
// 情況2:無參數,取值
(0 in this ? this[0].textContent : null) // 直接借用 textContent 屬性
},
attr: function(name, value){
var result
return
// 情況1:無第二個參數,讀取值(讀取值只能讀取第一個元素的值)
(typeof name == 'string' && !(1 in arguments)) ?
(!this.length || this[0].nodeType !== 1 ? undefined :
/*
注釋:
this[0]是一個DOM節點,有『屬性』也有『特性』
result = this[0].getAttribute(name) 試圖獲取 DOM節點屬性
name in this[0] 判斷是不是js對象的屬性
然后,該返回哪一個就返回哪一個
*/
(!(result = this[0].getAttribute(name)) && name in this[0]) ?
this[0][name] :
result
) :
// 情況2:有第二個參數,設置值(針對每個元素設置值)
this.each(function(idx){
if (this.nodeType !== 1) return
// 傳入的參數可能是一個對象集合
// 此時,是不是應該放在『情況1』當中???此時,value根本沒有用啊???
if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
// 傳入的不是對象,即設置一個單一的屬性。
// 但是,這里的 value 參數可以是一個函數
// funcArg 即處理了 value 是函數和非函數的兩種情況
else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
})
},
removeAttr: function(name){
return this.each(
function(){
this.nodeType === 1 && name.split(' ').forEach(
function(attribute){
setAttribute(this, attribute) // 將屬性設置為空,setAttribute會移除屬性
},
this // 改參數將成為 forEach 中函數的this
)
}
)
},
// 讀取、設置屬性(js對象的屬性)
prop: function(name, value){
// propMap 中存儲的:key是html中的屬性名稱,value是js對象中的屬性名稱
// 例如,html中的 "class" 在DOM對象中,就需要使用 "className" 這個名字讀取,同理于:for maxlength cellspacing 等等
name = propMap[name] || name
/*
上文定義:
propMap = {
'tabindex': 'tabIndex',
'readonly': 'readOnly',
'for': 'htmlFor',
'class': 'className',
'maxlength': 'maxLength',
'cellspacing': 'cellSpacing',
'cellpadding': 'cellPadding',
'rowspan': 'rowSpan',
'colspan': 'colSpan',
'usemap': 'useMap',
'frameborder': 'frameBorder',
'contenteditable': 'contentEditable'
}
*/
return
// 有第二個參數,設置屬性
(1 in arguments) ?
this.each(function(idx){
// 設置屬性值,funcArg處理函數或者非函數
this[name] = funcArg(this, value, idx, this[name])
}) :
// 無第二個參數,讀取屬性(讀取第一個元素的)
(this[0] && this[0][name])
},
// 前面加上 'data-' 通過 attr 設置或者讀取
data: function(name, value){
/*
上文定義:
capitalRE = /([A-Z])/g, //大寫字母
*/
// 前面加上 'data-' 將 'A' 替換為 '-a'
var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()
var data = (1 in arguments) ?
this.attr(attrName, value) :
this.attr(attrName)
return data !== null ? deserializeValue(data) : undefined
/*
上文定義的,deserializeValue 函數的作用是:
// 將字符串變成響應的對象或者值,例如源代碼的注釋:
// "true" => true
// "false" => false
// "null" => null
// "42" => 42
// "42.5" => 42.5
// "08" => "08"
// JSON => parse if valid
// String => self
*/
},
val: function(value){
return
// 有參數,設置值
0 in arguments ?
this.each(function(idx){
// 遍歷每個元素,直接對 value 屬性賦值
this.value = funcArg(this, value, idx, this.value)
}) :
// 無參數,讀取值
(this[0] && (
// 如果元素是 <select multiple> 多選列表
this[0].multiple ?
// 返回所有選中的option的值的數組
$(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') :
/*
上文定義:
pluck: function(property){
return $.map(this, function(el){ return el[property] })
},
*/
// 如果不是,直接獲取 value
this[0].value
)
)
},
// 獲取、設置元素的 offset
offset: function(coordinates){
// 如果有 coordinates 參數,設置坐標值,并返回當前對象
if (coordinates) return this.each(function(index){
var $this = $(this),
// 支持函數(傳入 $this.offset() 做參數)和非函數
coords = funcArg(this, coordinates, index, $this.offset()),
// 找到最近的 “relative”, “absolute” or “fixed” 的祖先元素,并獲取它的 offset()
parentOffset = $this.offsetParent().offset(),
// left 和 top 需要去掉定位的祖先元素的 left、top 值
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
// static時,設置 top、left是無效的
if ($this.css('position') == 'static') props['position'] = 'relative'
// 通過 css 賦值
$this.css(props)
})
// 當前對象是空,則返回 null
if (!this.length) return null
// 如果沒有 coordinates 參數,則返回第一個元素的坐標值
var obj = this[0].getBoundingClientRect()
/*
elem.getBoundingClientRect() 返回一個對象,
包含元素的 top bottom left right width height 的值
但是這個 top、bottom、left、right 是相對于瀏覽器窗口的距離,而不是頁面的邊界
(注意,elem.getBoundingClientRect()在IE低版本瀏覽器有2px的兼容問題)
window.pageXOffset 和 window.pageYOffset 可獲取網頁滾動的距離,
IE低版本需要用 document.body.scrollLeft 和 document.body.scrollTop 兼容
*/
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
},
// 設置、獲取 css
css: function(property, value){
// 只有一個參數,獲取第一個元素的樣式
if (arguments.length < 2) {
var computedStyle, element = this[0]
if(!element) return // 如果第一個元素無值,直接返回。否則繼續
// 獲取元素的計算后的樣式
computedStyle = getComputedStyle(element, '')
if (typeof property == 'string')
// 情況1,參數為字符串形式
// 先從elem內聯樣式獲取(element.style),此時需要 camelize(property) 轉換,如將 background-color 變為 backgroundColor
// 如果未找到,則從css樣式獲取 computedStyle.getPropertyValue(property)
// (重要)注釋:elem.style 只能獲取元素設置的內聯樣式、不能獲取css樣式;而 getComputedStyle 可獲取內聯、css樣式。
return element.style[camelize(property)] || computedStyle.getPropertyValue(property)
else if (isArray(property)) {
// 情況2,參數為數組形式(注意,此時 isObject 情況尚未判斷)
var props = {}
$.each(property, function(_, prop){
props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
})
return props // 返回一個對象
}
}
// 其他情況:有兩個參數、property是對象
var css = ''
if (type(property) == 'string') {
// 情況1,property 是字符串,設置單個樣式
if (!value && value !== 0)
// 如果value參數是 '' null undefined 則移除這個css樣式
// 注:此計算只適用于內聯樣式的刪除,對 css 樣式無效,因為它只通過 this.style.removeProperty 計算,而 this.style 獲取不到css樣式
this.each(function(){ this.style.removeProperty(dasherize(property)) })
else
// value有正常值,將 css 生成一個字符串(如 'font-size:20px')等待賦值給內聯樣式
// maybeAddPx(property, value) 需要增加 px 的增加上
css = dasherize(property) + ":" + maybeAddPx(property, value)
} else {
// 情況2,property 是對象(此時就不管第二個參數是什么了,不用第二個參數),一次性設置多個樣式
for (key in property)
if (!property[key] && property[key] !== 0)
// 如果對象屬性值是 '' null undefined 則移除這個css樣式,同理,只針對內聯樣式
this.each(function(){ this.style.removeProperty(dasherize(key)) })
else
// 否則,給 css 賦值一個字符串,多樣式屬性用 ; 隔開
css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
}
// 針對每個元素,設置內聯樣式(this.style.cssText可獲取、設置內聯樣式)
// 最后返回自身
return this.each(function(){ this.style.cssText += ';' + css })
/*
上文定義:
// 將 lineHeight 轉換為 line-height 格式
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()
}
*/
},
// 獲取一個元素的索引值(從0開始計數)。當elemen參數沒有給出時,返回當前元素在兄弟節點中的位置
index: function(element){
/*
上文定義:
$.fn.indexOf: emptyArray.indexOf
*/
// 其實 this 本身就是一個數組,數組本身就有 indexOf ,為何還要上文的這個賦值呢?
// 因為上文中,this.__proto__ 修改了,不是 Array.prototype 了,也就沒有 indexOf 方法了
// 因此要手動賦值,需要將數組常用的方法在重新賦值給 $.fn.indexOf
return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0])
},
hasClass: function(name){
if (!name) return false
return emptyArray.some.call(this, function(el){
// this 就是 classRE(name) 的返回值(返回一個正則)
// function className(node, value){...} 獲取或者設置elem的className
return this.test(className(el))
}, classRE(name))
// array.some(callback,[ thisObject]); 只要數組中一項符合callback要求,即返回true
/*
// 上文定義 classCache = {}
function classRE(name) {
return name in classCache ?
classCache[name] :
(classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
// classCache 存儲的數據是這樣的:
// {
// abc: /(^|\s)abc(\s|$)/, // 能匹配 'abc' 或 ' abc ' 或 ' abc' 或 'abc '
// xyz: /(^|\s)abc(\s|$)/,
// ...
// }
}
*/
},
addClass: function(name){
if (!name) return this
// 針對所有元素都添加className,最終返回本身
return this.each(function(idx){
// 說明當前元素不是 DOM node
if (!('className' in this)) return
// classList 是一開始就定義的空變量
classList = []
// 獲取元素的 clasname // 支持傳入函數
var cls = className(this), newName = funcArg(this, name, idx, cls)
// 把要賦值的值,按照空白分組,遍歷
newName.split(/\s+/g).forEach(function(klass){
// 把當前元素不存在的class,push到classlist中
if (!$(this).hasClass(klass)) classList.push(klass)
}, this)
// 如果classlist有數據,則為當前元素賦值最新的class值(現有的classname和新的classname拼接)
classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
})
},
removeClass: function(name){
// 針對所有元素都移除className,最終返回本身
return this.each(function(idx){
// 說明當前元素不是 DOM node
if (!('className' in this)) return
// 如果參數空,則移除元素的所有class
if (name === undefined) return className(this, '')
// 獲取現有的classname
classList = className(this)
// (可以傳入函數)遍歷新的classname字符串
funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){
// classRE(klass) 返回一個正則,匹配 'classname' 或 ' classname ' 或 ' classname' 或 'classname '
// 針對傳入的classname字符串,對每個符合條件的classname,都替換為 ' '(即刪除了)
classList = classList.replace(classRE(klass), " ")
})
// 對整理好的classname,重新賦值給當前元素
className(this, classList.trim())
})
},
toggleClass: function(name, when){
// when 參數相當于一個條件:
// 如果 when === true 則單純執行 addClass
// 如果 when === false 則單純執行 removeClass
if (!name) return this
return this.each(function(idx){
// name 可接收函數,可以是空白分割開來的多個classname
var $this = $(this), names = funcArg(this, name, idx, className(this))
// 用空白分割開多個class
names.split(/\s+/g).forEach(function(klass){
// 如果有 when 參數,則只通過when參數判斷,true則只執行addClass,false則只執行removeClass
// 如果沒有 when 參數,則判斷元素有沒有該class,有則移除,沒有則添加
(when === undefined ? !$this.hasClass(klass) : when) ?
$this.addClass(klass) : $this.removeClass(klass)
})
})
},
scrollTop: function(value){
if (!this.length) return
// 普通elem有 scrollTop 屬性,可以獲取或者設置top值
// window對象沒有 scrollTop 屬性,通過 pageYOffset 獲取,通過 scrollTo() 賦值
var hasScrollTop = 'scrollTop' in this[0]
// value 無值,獲取 top
if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
// value 有值,設置 top
return this.each(hasScrollTop ?
function(){ this.scrollTop = value } :
function(){ this.scrollTo(this.scrollX, value) }) // window.scrollX 獲取橫向滾動值
},
scrollLeft: function(value){
if (!this.length) return
var hasScrollLeft = 'scrollLeft' in this[0]
if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
return this.each(hasScrollLeft ?
function(){ this.scrollLeft = value } :
function(){ this.scrollTo(value, this.scrollY) }) // window.scrollX 獲取縱向滾動值
},
position: function() {
if (!this.length) return
var elem = this[0],
// Get *real* offsetParent
offsetParent = this.offsetParent(), // 找到第一個定位過的祖先元素 “relative”, “absolute” or “fixed”
// Get correct offsets
offset = this.offset(), // 獲取自身的offset
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset() // 獲取定位祖先元素的offset( body、html直接設置 top:0;left:0 )
// 上文定義: rootNodeRE = /^(?:body|html)$/i,
// 去掉當前元素的 margin 寬度
// Subtract element margins
// note: when an element has margin: auto the offsetLeft and marginLeft
// are the same in Safari causing offset.left to incorrectly be 0
offset.top -= parseFloat( $(elem).css('margin-top') ) || 0
offset.left -= parseFloat( $(elem).css('margin-left') ) || 0
// 增加父元素的 border 寬度
// Add offsetParent borders
parentOffset.top += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0
parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0
// Subtract the two offsets
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
},
offsetParent: function() {
// 通過 this.map 遍歷當前對象所有元素,進行計算,然后拼接新的數組,并返回。保證鏈式操作
return this.map(function(){
var parent = this.offsetParent || document.body // elem.offsetParent 可返回最近的改元素最近的已經定位的父元素
while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
// 如果獲取的parent不是null、不是body或html、而且position==static
// 則繼續向上查找 offsetParent、大不了找到 body 為止
parent = parent.offsetParent
// 最后返回改元素
return parent
})
}
}
// for now
$.fn.detach = $.fn.remove
// Generate the `width` and `height` functions
;['width', 'height'].forEach(function(dimension){
// 將 width height 變為 Width Height
var dimensionProperty =
dimension.replace(/./, function(m){ return m[0].toUpperCase() })
$.fn[dimension] = function(value){
var offset, el = this[0]
// 情況1,無參數,獲取第一個元素的值
if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] : // window.innerHeight
isDocument(el) ? el.documentElement['scroll' + dimensionProperty] : // document.documentElement.scrollHeight
(offset = this.offset()) && offset[dimension] // this.offset().width
// 情況2,有參數,設置所有元素的值
else return this.each(function(idx){
el = $(this)
// 通過 css() 方法設置,支持傳入函數
el.css(dimension, funcArg(this, value, idx, el[dimension]()))
})
}
})
// 針對當前元素、遍歷子元素,都執行 fun 函數
function traverseNode(node, fun) {
fun(node)
for (var i = 0, len = node.childNodes.length; i < len; i++)
traverseNode(node.childNodes[i], fun)
}
// 上文定義 adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ],
// Generate the `after`, `prepend`, `before`, `append`,
// `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods.
adjacencyOperators.forEach(function(operator, operatorIndex) {
var inside = operatorIndex % 2 //=> prepend, append
$.fn[operator] = function(){
// arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
var argType, nodes = $.map(arguments, function(arg) {
argType = type(arg)
return argType == "object" || argType == "array" || arg == null ?
arg : zepto.fragment(arg)
}),
parent, copyByClone = this.length > 1
if (nodes.length < 1) return this
return this.each(function(_, target){
parent = inside ? target : target.parentNode
// convert all methods to a "before" operation
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
var parentInDocument = $.contains(document.documentElement, parent)
nodes.forEach(function(node){
if (copyByClone) node = node.cloneNode(true)
else if (!parent) return $(node).remove()
parent.insertBefore(node, target)
if (parentInDocument) traverseNode(node, function(el){
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src)
window['eval'].call(window, el.innerHTML)
})
})
})
}
// after => insertAfter
// prepend => prependTo
// before => insertBefore
// append => appendTo
$.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
$(html)[operator](this)
return this
}
})
zepto.Z.prototype = $.fn
// Export internal API functions in the `$.zepto` namespace
zepto.uniq = uniq
zepto.deserializeValue = deserializeValue
$.zepto = zepto
return $
})()
window.Zepto = Zepto
window.$ === undefined && (window.$ = Zepto)
```