Zepto中数据缓存原理与实现

本文作者:IMWeb 谦龙 原文出处:IMWeb社区 未经同意,禁止转载

前言

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

原文链接

源码仓库

<!-- more -->

原理

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

得到的数据如上图所示,接下来我们再回到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)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏python成长之路

文件常用操作

1679
来自专栏HTML5学堂

2015.12.24 HTML5真题练习

HTML5学堂:各位,圣诞快乐~!!!每天一道题,强壮程序员!今日主要涉及12.23日关于逗号运算符和for循环知识的题目解答,以及一道涉及逗号运算符的题目。 ...

2795
来自专栏狮乐园

高级 Angular 组件模式 (5)

在之前的例子中,已经出现多次使用template reference variable(模板引用变量)的场景,现在让我们来深入研究如何通过使用模板引用变量来关联...

912
来自专栏技术沉淀

Python: set实例透析

1062
来自专栏小壮和前端

js写插件教程

2151
来自专栏用户画像

H5中的标记方法

要使用H5标记,必须先进行如下的doctype声明,不区分大小写。Web浏览器通过判断文件开头有没有这个声明,来判断解析器和渲染类型是否切换到对应的H5模式。

811
来自专栏程序员的SOD蜜

C#中?与??的区别

起初我也不知道C#中有??操作符,今天张鹏在查看我的MVC示例程序的时候问了这个问题,检查代码后发现,下面的代码是VS2010在生成MVC应用程序自己添加的: ...

2337
来自专栏xiaoxi666的专栏

c++ 继承类强制转换时的虚函数表工作原理

本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解。

1853
来自专栏编程心路

想学习php的,不如来这里看看

win+R打开命令行,cmd进DOS窗口 DOS命令开启关闭Apache和Mysql Apache启动关闭命令

1203
来自专栏九彩拼盘的叨叨叨

Sass 写法示例

CSS 本身是非常强大的,但随着样式表变大,变复杂,维护 CSS 变得越来越难。这时候预处理就有用了。Sass 是一种预处理,它能让你使用一些 CSS 中没有的...

881

扫码关注云+社区

领取腾讯云代金券