Zepto中数据缓存原理与实现

前言

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

原文链接

源码仓库

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

得到的数据如上图所示,接下来我们再回到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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,951评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,621评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 我想变成白云, 为人们遮住那, 无比炎热的太阳! 图片发自简书App
    宋鑫鱼阅读 241评论 0 0
  • 木木以前很怕黑,一到晚上睡觉时就会对我说:“妈妈,我害怕,我会做噩梦!”我对她说:“你给老爷(神)磕个头让牠保...
    木妈阅读 639评论 5 0