前言
以前我們使用Zepto進(jìn)行開發(fā)的時候,會把一些自定義的數(shù)據(jù)存到dom節(jié)點(diǎn)上,好處是非常直觀和便捷,但是也帶來了例如直接將數(shù)據(jù)暴露出來會出現(xiàn)安全問題,數(shù)據(jù)以html自定義屬性標(biāo)簽存在,對于瀏覽器本身來說是沒有多大意義的,最后要獲取數(shù)據(jù)的時候還得操作dom。Zepto有一個
data模塊,專門用來做數(shù)據(jù)緩存,允許我們存放任何與dom相關(guān)的數(shù)據(jù)。

原理
在開始學(xué)習(xí)和閱讀Zepto中的data模塊前,我們先大致了解一下dom元素和要緩存的數(shù)據(jù)是如何聯(lián)系起來的。

看一下上面那張圖。簡單地理解就是
- dom元素身上有一
exp(Zepto1507010934916)屬性,其對應(yīng)的值是1,2,3整數(shù)數(shù)字, - data是一個存儲著與dom元素相關(guān)聯(lián)的自定義數(shù)據(jù)的大對象類似下面這樣
{
1: {
name: 'qianlongo'
},
2: {
sex: 'boy'
}
}
dom元素就是通過1,2,3數(shù)字索引和大對象data關(guān)聯(lián)起來
對于DOM自定義數(shù)據(jù)的增刪改查就是在對數(shù)字索引對應(yīng)的對象進(jìn)行操作。
$.fn.data
在匹配元素上存儲任意相關(guān)數(shù)據(jù)或返回匹配的元素集合中的第一個元素的給定名稱的數(shù)據(jù)存儲的值。
例子
<div class="box" data-name="qianlongo" data-sex="boy"></div>
let $box = $('.box')
// setData
$box.data("foo", 52)
$box.data("bar", { myType: "test", count: 40 })
$box.data({ baz: [ 1, 2, 3 ] })
// getData
$box.data("foo") // 52
$box.data("name") // qianlongo
$box.data() // { name: "qianlongo", sex: "boy", foo: 52, bar: { myType: "test", count: 40 }, baz: [ 1, 2, 3 ] }
基本用法大家肯定很熟悉,需要注意的地方是,我們也可以直接獲取定義在html標(biāo)簽上以data-為前綴的屬性。接下來我們就直接看源碼實(shí)現(xiàn)啦
源碼
$.fn.data = function(name, value) {
return value === undefined ?
// set multiple values via object
$.isPlainObject(name) ?
this.each(function(i, node){
$.each(name, function(key, value){ setData(node, key, value) })
}) :
// get value from first element
(0 in this ? getData(this[0], name) : undefined) :
// set value on all elements
this.each(function(){ setData(this, name, value) })
}
通過上面的例子我們知道,設(shè)置數(shù)據(jù)的時候可以單個屬性設(shè)置,也可以多個屬性(傳遞一個對象)一起設(shè)置。大量使用三目運(yùn)算是Zepto一貫的風(fēng)格。我們來拆解一下這段代碼。
- 當(dāng)value傳遞了值并且不是undefined的時候可以認(rèn)為是設(shè)置單個數(shù)據(jù)屬性。于是走這段代碼
this.each(function(){ setData(this, name, value) })
通過遍歷匹配元素,并調(diào)用setData方法傳入元素,要設(shè)置的數(shù)據(jù)的key和value。
- 當(dāng)沒有傳遞value進(jìn)來,并且name是個純粹的對象時候。也就是類似這樣使用
$box.data({ baz: [ 1, 2, 3 ] })
此時走的是這段代碼
this.each(function(i, node){
$.each(name, function(key, value){ setData(node, key, value) })
})
還是遍歷當(dāng)前匹配元素,并且遍歷傳進(jìn)的對象name,到底層還是調(diào)用setData方法一個個屬性進(jìn)行設(shè)置。
- 當(dāng)name不是一個對象的時候,認(rèn)為是對數(shù)據(jù)的讀取操作。走的是這段代碼
(0 in this ? getData(this[0], name) : undefined)
通過判斷當(dāng)前是否有匹配的元素,如果有則是調(diào)用getData方法,并傳入匹配元素集合中的第一個元素,以及要獲取的數(shù)據(jù)name屬性。如果沒有匹配元素,就直接返回undefined了。
總體邏輯還是挺清晰的。接下來我們主要需要弄清楚上面用到的幾個函數(shù)setData,getData。以及解釋一下data模塊初始定義的幾個變量
var data = {},
dataAttr = $.fn.data,
camelize = $.camelCase,
exp = $.expando = 'Zepto' + (+new Date())
各變量解釋如下
/**
* data 存儲于dom相映射的數(shù)據(jù)數(shù)據(jù)結(jié)構(gòu)如同下
* {
* 1: {
* name: 'qianlongo',
* sex: 'boy'
* },
* 2: {
* age: 100
* }
* }
*
* dataAttr $原型上的data方法,通過getAttribute和setAttribute設(shè)置或讀取元素屬性
* camelize 中劃線轉(zhuǎn)小駝峰函數(shù)
* exp => Zepto1507004986420 設(shè)置在dom上的屬性,value是data中的key 1, 2,3等
*/
setData
function setData(node, name, value) {
var id = node[exp] || (node[exp] = ++$.uuid),
store = data[id] || (data[id] = attributeData(node))
if (name !== undefined) store[camelize(name)] = value
return store
}
exp是類似Zepto1507004986420的字符串,$.uuid初始值是0,首先會嘗試去讀取元素身上的exp屬性,元素沒有該屬性就為該元素設(shè)置exp屬性。
并去data大對象中讀取id(1, 2, 3...)屬性,當(dāng)然了如果data對象中沒有讀取到,就通過調(diào)用attributeData函數(shù)先獲取
node節(jié)點(diǎn)所有以data-為前綴的自定義屬性,并將其賦值。
現(xiàn)在自定義屬性的集合已經(jīng)有了,先判斷name是否是個undefined,不是就往store上添加name屬性。
最后函數(shù)調(diào)用之后會返回整個數(shù)據(jù)對象store。
attributeData
獲取元素以data-為前綴的自定義屬性的集合
// Read all "data-*" attributes from a node
function attributeData(node) {
var store = {}
$.each(node.attributes || emptyArray, function(i, attr){
if (attr.name.indexOf('data-') == 0)
store[camelize(attr.name.replace('data-', ''))] =
$.zepto.deserializeValue(attr.value)
})
return store
}
我們先來看一下node.attributes mdn是個啥
Element.attributes 屬性返回該元素所有屬性節(jié)點(diǎn)的一個實(shí)時集合。該集合是一個 NamedNodeMap 對象,不是一個數(shù)組,所以它沒有 數(shù)組 的方法,其包含的 屬性 節(jié)點(diǎn)的索引順序隨瀏覽器不同而不同。更確切地說,attributes 是字符串形式的名/值對,每一對名/值對對應(yīng)一個屬性節(jié)點(diǎn)。
例子
<div class="box" data-name="qianlongo" data-sex="boy" foo="foo" title="標(biāo)題"></div>
let $box = document.querySelector('.box')
$box.dataset.age = 100
console.log($box.attributes)

得到的數(shù)據(jù)如上圖所示,接下來我們再回到attributeData函數(shù)的源碼分析
if (attr.name.indexOf('data-') == 0)
store[camelize(attr.name.replace('data-', ''))] =
$.zepto.deserializeValue(attr.value)
通過判斷ele.attributes拿到的集合中,是否是以data-開頭的屬性,如果是就往store對象中添加駝峰化后的該屬性,并且序列化之后的attr.value作為該屬性的值。最后將store對象返回。
getData
獲取存儲在data中與DOM元素關(guān)聯(lián)的對象name屬性。當(dāng)name屬性不存在的時候直接返回整個對象。
function getData(node, name) {
var id = node[exp], store = id && data[id]
if (name === undefined) return store || setData(node)
else {
if (store) {
if (name in store) return store[name]
var camelName = camelize(name)
if (camelName in store) return store[camelName]
}
return dataAttr.call($(node), name)
}
}
實(shí)現(xiàn)思路還是首先去讀取setData時候添加在node節(jié)點(diǎn)上的id,然后以該id為key去data中查找。如果name沒有傳,此時直接返回整個store,當(dāng)然如果store也沒有找到,就返回調(diào)用setData后返回的該元素的自定義屬性的集合。
當(dāng)store存在時,先判斷name屬性在store中存在與否,存在便直接返回相應(yīng)的屬性,否則對傳入的name進(jìn)行駝峰化之后再判斷在store中是否存在,存在即返回對應(yīng)的屬性。也就是說你傳入的name為min-age或者minAge得到的是一樣的值。
最后如果在數(shù)據(jù)緩存中還沒有找到屬性name,就調(diào)用dataAttr函數(shù),去直接查找元素身上的相關(guān)屬性。
removeData
在元素上移除綁定的數(shù)據(jù)
可以添加或者更新數(shù)據(jù)自然也就可以移除數(shù)據(jù)了,先看下例子
例子
<div class="box"></div>
let $box = $('.box')
$box.data("foo", 52)
$box.data("bar", { myType: "test", count: 40 })
$box.data({ baz: [ 1, 2, 3 ] })
// $box.removeData('foo')
// $box.removeData('foo bar baz')
// $box.removeData(['foo', 'bar', 'baz'])
// $box.removeData()
我們可以指定刪除單個屬性,也可以通過空格隔開刪除多個屬性,也可以傳入一個要刪除的屬性數(shù)組,甚至當(dāng)你什么都不傳的時候,原先設(shè)置在該元素身上的data會被全部清空
源碼
$.fn.removeData = function(names) {
if (typeof names == 'string') names = names.split(/\s+/)
return this.each(function(){
var id = this[exp], store = id && data[id]
if (store) $.each(names || store, function(key){
delete store[names ? camelize(this) : key]
})
})
}
首先傳進(jìn)來的names是字符串的情況下,先轉(zhuǎn)化成數(shù)組,接著就是對當(dāng)前匹配的元素集合進(jìn)行遍歷,逐個刪除元素對應(yīng)的緩存的數(shù)據(jù)。
當(dāng)查找到store的時候?qū)D(zhuǎn)化后的names或者store進(jìn)行遍歷,如果是自己指定要刪除的屬性,先駝峰化一下,再用delete刪除,否則全部清空則直接delete store中的key
$.data
存儲任意數(shù)據(jù)到指定的元素并且/或者返回設(shè)置的值
$.data = function(elem, name, value) {
return $(elem).data(name, value)
}
定義在$函數(shù)身上的靜態(tài)方法,底層還是調(diào)用的實(shí)例方法.data。
$.hasData
確定元素是否有與之相關(guān)的Zepto數(shù)據(jù)。
$.hasData = function(elem) {
var id = elem[exp], store = id && data[id]
return store ? !$.isEmptyObject(store) : false
}
同樣定義在$函數(shù)身上的靜態(tài)方法,原理就是拿著elem身上的id,去data中查找是否有與之關(guān)聯(lián)的數(shù)據(jù)對象,如果找到了并且不是一個空對象,便返回true,否則沒有找到或者是空對象都是返回false
remove, empty
生成擴(kuò)展的remove和empty方法,未擴(kuò)展之前的remove和empty功能依舊還在,增添了刪除選中的元素緩存的數(shù)據(jù)功能。
;['remove', 'empty'].forEach(function(methodName){
// 緩存原型上之前對應(yīng)的remove和empty方法
var origFn = $.fn[methodName]
// 重寫兩個方法
$.fn[methodName] = function() {
// 獲取當(dāng)前選中元素的所有內(nèi)部包含元素
var elements = this.find('*')
// 如果是remove方法,則在獲取的elements元素基礎(chǔ)上把本身也添加進(jìn)去
if (methodName === 'remove') elements = elements.add(this)
// 調(diào)用removeData刪除與dom關(guān)聯(lián)的data中的數(shù)據(jù)
elements.removeData()
// 最后還是調(diào)用對應(yīng)的方法刪除dom,或者清除dom的內(nèi)容
return origFn.call(this)
}
})
結(jié)尾
以上是Zepto種data模塊所有源碼分析,歡迎大家指正其中有問題的地方。
文章記錄
data模塊
- Zepto中數(shù)據(jù)緩存原理與實(shí)現(xiàn)(2017-10-03)
form模塊
- zepto源碼分析之form模塊(2017-10-01)
zepto模塊
- 這些Zepto中實(shí)用的方法集(2017-08-26)
- Zepto核心模塊之工具方法拾遺 (2017-08-30)
- 看zepto如何實(shí)現(xiàn)增刪改查DOM (2017-10-2)
event模塊
- mouseenter與mouseover為何這般糾纏不清?(2017-06-05)
- 向zepto.js學(xué)習(xí)如何手動觸發(fā)DOM事件(2017-06-07)
- 誰說你只是"會用"jQuery?(2017-06-08)
ajax模塊
- 原來你是這樣的jsonp(原理與具體實(shí)現(xiàn)細(xì)節(jié))(2017-06-11)