Zepto中資料快取原理與實現

謙龍發表於2017-10-03

前言

以前我們使用Zepto進行開發的時候,會把一些自定義的資料存到dom節點上,好處是非常直觀和便捷,但是也帶來了例如直接將資料暴露出來會出現安全問題,資料以html自定義屬性標籤存在,對於瀏覽器本身來說是沒有多大意義的,最後要獲取資料的時候還得操作dom。Zepto有一個data模組,專門用來做資料快取,允許我們存放任何與dom相關的資料。

原文連結

原始碼倉庫

data
data

原理

在開始學習和閱讀Zepto中的data模組前,我們先大致瞭解一下dom元素和要快取的資料是如何聯絡起來的。

原理
原理

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

  • dom元素身上有一exp(Zepto1507010934916)屬性,其對應的值是1,2,3整數數字,
  • data是一個儲存著與dom元素相關聯的自定義資料的大物件類似下面這樣

{
  1: {
    name: 'qianlongo'
  },
  2: {
    sex: 'boy'
  }
}複製程式碼
  • dom元素就是通過1,2,3數字索引和大物件data關聯起來

  • 對於DOM自定義資料的增刪改查就是在對數字索引對應的物件進行操作。

$.fn.data

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

例子

<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)

相關文章