资讯专栏INFORMATION COLUMN

Zepto中数据缓存原理与实现

macg0406 / 2009人阅读

摘要:有一个模块,专门用来做数据缓存,允许我们存放任何与相关的数据。在匹配元素上存储任意相关数据或返回匹配的元素集合中的第一个元素的给定名称的数据存储的值。确定元素是否有与之相关的数据。

前言

以前我们使用Zepto进行开发的时候,会把一些自定义的数据存到dom节点上,好处是非常直观和便捷,但是也带来了例如直接将数据暴露出来会出现安全问题,数据以html自定义属性标签存在,对于浏览器本身来说是没有多大意义的,最后要获取数据的时候还得操作dom。Zepto有一个data模块,专门用来做数据缓存,允许我们存放任何与dom相关的数据。

原文链接

源码仓库

原理

在开始学习和阅读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

在匹配元素上存储任意相关数据或返回匹配的元素集合中的第一个元素的给定名称的数据存储的值。

例子

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一贯的风格。我们来拆解一下这段代码。

当value传递了值并且不是undefined的时候可以认为是设置单个数据属性。于是走这段代码

this.each(function(){ setData(this, name, value) })

通过遍历匹配元素,并调用setData方法传入元素,要设置的数据的key和value。

当没有传递value进来,并且name是个纯粹的对象时候。也就是类似这样使用

$box.data({ baz: [ 1, 2, 3 ] })

此时走的是这段代码

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

还是遍历当前匹配元素,并且遍历传进的对象name,到底层还是调用setData方法一个个属性进行设置。

当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 是字符串形式的名/值对,每一对名/值对对应一个属性节点。

例子

let $box = document.querySelector(".box")
    $box.dataset.age = 100
    console.log($box.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

在元素上移除绑定的数据

可以添加或者更新数据自然也就可以移除数据了,先看下例子

例子

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模块

Zepto中数据缓存原理与实现(2017-10-03)

form模块

zepto源码分析之form模块(2017-10-01)

zepto模块

这些Zepto中实用的方法集(2017-08-26)

Zepto核心模块之工具方法拾遗 (2017-08-30)

看zepto如何实现增删改查DOM (2017-10-2)

event模块

mouseenter与mouseover为何这般纠缠不清?(2017-06-05)

向zepto.js学习如何手动触发DOM事件(2017-06-07)

谁说你只是"会用"jQuery?(2017-06-08)

ajax模块

原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88834.html

相关文章

  • Zepto源码之Data模块

    摘要:的模块用来获取节点中的属性的数据,和储存跟相关的数据。获取节点指定的缓存值。如果存在,则删除指定的数据,否则将缓存的数据全部删除。为所有下级节点,如果为方法,则节点自身也是要被移除的,所以需要将自身也加入到节点中。 Zepto 的 Data 模块用来获取 DOM 节点中的 data-* 属性的数据,和储存跟 DOM 相关的数据。 读 Zepto 源码系列文章已经放到了github上,欢...

    wua_wua2012 评论0 收藏0
  • Zepto这样操作元素属性

    摘要:还有一点需要注意的是方法设置或者获取都是在操作元素的属性,那它和,的区别在哪呢可以查看设置设置与的设置部分比较类似,既支持直接传入普通的字符串也支持传入回调函数。 前言 使用Zepto的时候,我们经常会要去操作一些DOM的属性,或元素本身的固有属性或自定义属性等。比如常见的有attr(),removeAttr(),prop(),removeProp(),data()等。接下来我们挨个整...

    付伦 评论0 收藏0
  • Zepto学习关于"偏移"的那些事

    摘要:获得当前元素相对于的位置。返回一个对象含有和当给定一个含有和属性对象时,使用这些值来对集合中每一个元素进行相对于的定位。获取对象集合中第一个元素相对于其的位置。结尾以上就是中与偏移相关的几个的解析,欢迎指出其中的问题和有错误的地方。 前言 这篇文章主要想说一下Zepto中与偏移相关的一些事,很久很久以前,我们经常会使用offset、position、scrollTop、scrollLe...

    hzx 评论0 收藏0

发表评论

0条评论

macg0406

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<