Zepto中數據緩存原理與實現

前言

之前咱們使用Zepto進行開發的時候,會把一些自定義的數據存到dom節點上,好處是很是直觀和便捷,可是也帶來了例如直接將數據暴露出來會出現安全問題,數據以html自定義屬性標籤存在,對於瀏覽器自己來講是沒有多大意義的,最後要獲取數據的時候還得操做dom。Zepto有一個data模塊,專門用來作數據緩存,容許咱們存聽任何與dom相關的數據。javascript

原文連接html

源碼倉庫java

data
data

原理

在開始學習和閱讀Zepto中的data模塊前,咱們先大體瞭解一下dom元素和要緩存的數據是如何聯繫起來的。node

原理
原理

看一下上面那張圖。簡單地理解就是git

  • dom元素身上有一exp(Zepto1507010934916)屬性,其對應的值是1,2,3整數數字,
  • data是一個存儲着與dom元素相關聯的自定義數據的大對象相似下面這樣
{
  1: {
    name: 'qianlongo'
  },
  2: {
    sex: 'boy'
  }
}複製代碼
  • dom元素就是經過1,2,3數字索引和大對象data關聯起來github

  • 對於DOM自定義數據的增刪改查就是在對數字索引對應的對象進行操做。ajax

$.fn.data

在匹配元素上存儲任意相關數據或返回匹配的元素集合中的第一個元素的給定名稱的數據存儲的值。json

例子數組

<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標籤上以data-爲前綴的屬性。接下來咱們就直接看源碼實現啦瀏覽器

源碼

$.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) })
}複製代碼

經過上面的例子咱們知道,設置數據的時候能夠單個屬性設置,也能夠多個屬性(傳遞一個對象)一塊兒設置。大量使用三目運算是Zepto一向的風格。咱們來拆解一下這段代碼。

  1. 當value傳遞了值而且不是undefined的時候能夠認爲是設置單個數據屬性。因而走這段代碼
this.each(function(){ setData(this, name, value) })複製代碼

經過遍歷匹配元素,並調用setData方法傳入元素,要設置的數據的key和value。

  1. 當沒有傳遞value進來,而且name是個純粹的對象時候。也就是相似這樣使用
$box.data({ baz: [ 1, 2, 3 ] })複製代碼

此時走的是這段代碼

this.each(function(i, node){
  $.each(name, function(key, value){ setData(node, key, value) })
})複製代碼

仍是遍歷當前匹配元素,而且遍歷傳進的對象name,到底層仍是調用setData方法一個個屬性進行設置。

  1. 當name不是一個對象的時候,認爲是對數據的讀取操做。走的是這段代碼
(0 in this ? getData(this[0], name) : undefined)複製代碼

經過判斷當前是否有匹配的元素,若是有則是調用getData方法,並傳入匹配元素集合中的第一個元素,以及要獲取的數據name屬性。若是沒有匹配元素,就直接返回undefined了。

整體邏輯仍是挺清晰的。接下來咱們主要須要弄清楚上面用到的幾個函數setData,getData。以及解釋一下data模塊初始定義的幾個變量

var data = {}, 
    dataAttr = $.fn.data, 
    camelize = $.camelCase,
    exp = $.expando = 'Zepto' + (+new Date())複製代碼

各變量解釋以下

/** * data 存儲於dom相映射的數據數據結構如同下 * { * 1: { * name: 'qianlongo', * sex: 'boy' * }, * 2: { * age: 100 * } * } * * dataAttr $原型上的data方法,經過getAttribute和setAttribute設置或讀取元素屬性 * camelize 中劃線轉小駝峯函數 * exp => Zepto1507004986420 設置在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屬性,元素沒有該屬性就爲該元素設置exp屬性。

並去data大對象中讀取id(1, 2, 3...)屬性,固然了若是data對象中沒有讀取到,就經過調用attributeData函數先獲取
node節點全部以data-爲前綴的自定義屬性,並將其賦值。

如今自定義屬性的集合已經有了,先判斷name是不是個undefined,不是就往store上添加name屬性。

最後函數調用以後會返回整個數據對象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 屬性返回該元素全部屬性節點的一個實時集合。該集合是一個 NamedNodeMap 對象,不是一個數組,因此它沒有 數組 的方法,其包含的 屬性 節點的索引順序隨瀏覽器不一樣而不一樣。更確切地說,attributes 是字符串形式的名/值對,每一對名/值對對應一個屬性節點。

例子

<div class="box" data-name="qianlongo" data-sex="boy" foo="foo" title="標題"></div>複製代碼
let $box = document.querySelector('.box')
    $box.dataset.age = 100
    console.log($box.attributes)複製代碼

attributes
attributes

獲得的數據如上圖所示,接下來咱們再回到attributeData函數的源碼分析

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元素關聯的對象name屬性。當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)
  }
}複製代碼

實現思路仍是首先去讀取setData時候添加在node節點上的id,而後以該id爲key去data中查找。若是name沒有傳,此時直接返回整個store,固然若是store也沒有找到,就返回調用setData後返回的該元素的自定義屬性的集合。

當store存在時,先判斷name屬性在store中存在與否,存在便直接返回相應的屬性,不然對傳入的name進行駝峯化以後再判斷在store中是否存在,存在即返回對應的屬性。也就是說你傳入的name爲min-age或者minAge獲得的是同樣的值。

最後若是在數據緩存中尚未找到屬性name,就調用dataAttr函數,去直接查找元素身上的相關屬性。

removeData

在元素上移除綁定的數據

能夠添加或者更新數據天然也就能夠移除數據了,先看下例子

例子

<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()複製代碼

咱們能夠指定刪除單個屬性,也能夠經過空格隔開刪除多個屬性,也能夠傳入一個要刪除的屬性數組,甚至當你什麼都不傳的時候,原先設置在該元素身上的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]
    })
  })
}複製代碼

首先傳進來的names是字符串的狀況下,先轉化成數組,接着就是對當前匹配的元素集合進行遍歷,逐個刪除元素對應的緩存的數據。

當查找到store的時候對轉化後的names或者store進行遍歷,若是是本身指定要刪除的屬性,先駝峯化一下,再用delete刪除,不然所有清空則直接delete store中的key

$.data

存儲任意數據到指定的元素而且/或者返回設置的值

$.data = function(elem, name, value) {
  return $(elem).data(name, value)
}複製代碼

定義在$函數身上的靜態方法,底層仍是調用的實例方法.data。

$.hasData

肯定元素是否有與之相關的Zepto數據。

$.hasData = function(elem) {
  var id = elem[exp], store = id && data[id]
  return store ? !$.isEmptyObject(store) : false
}複製代碼

一樣定義在$函數身上的靜態方法,原理就是拿着elem身上的id,去data中查找是否有與之關聯的數據對象,若是找到了而且不是一個空對象,便返回true,不然沒有找到或者是空對象都是返回false

remove, empty

生成擴展的remove和empty方法,未擴展以前的remove和empty功能依舊還在,增添了刪除選中的元素緩存的數據功能。

;['remove', 'empty'].forEach(function(methodName){
  // 緩存原型上以前對應的remove和empty方法
  var origFn = $.fn[methodName]
  // 重寫兩個方法
  $.fn[methodName] = function() {
    // 獲取當前選中元素的全部內部包含元素
    var elements = this.find('*')
    // 若是是remove方法,則在獲取的elements元素基礎上把自己也添加進去
    if (methodName === 'remove') elements = elements.add(this)
    // 調用removeData刪除與dom關聯的data中的數據
    elements.removeData()
    // 最後仍是調用對應的方法刪除dom,或者清除dom的內容
    return origFn.call(this)
  }
})複製代碼

結尾

以上是Zepto種data模塊全部源碼分析,歡迎你們指正其中有問題的地方。

文章記錄

data模塊

  1. Zepto中數據緩存原理與實現(2017-10-03)

form模塊

  1. zepto源碼分析之form模塊(2017-10-01)

zepto模塊

  1. 這些Zepto中實用的方法集(2017-08-26)
  2. Zepto核心模塊之工具方法拾遺 (2017-08-30)
  3. 看zepto如何實現增刪改查DOM (2017-10-2)

event模塊

  1. mouseenter與mouseover爲什麼這般糾纏不清?(2017-06-05)
  2. 向zepto.js學習如何手動觸發DOM事件(2017-06-07)
  3. 誰說你只是"會用"jQuery?(2017-06-08)

ajax模塊

  1. 原來你是這樣的jsonp(原理與具體實現細節)(2017-06-11)
相關文章
相關標籤/搜索