当前位置: 首页 > Web前端 > HTML5

Zepto中数据缓存的原理与实现

时间:2023-04-05 16:21:24 HTML5

前言我们在使用Zepto进行开发的时候,会在dom节点上存储一些自定义的数据。优点是非常直观方便,但也带来了数据直接暴露等问题。出来的时候会有安全问题。数据以html自定义属性标签的形式存在,对浏览器本身没有意义。最后,想要获取数据的时候,还得操作dom。Zepto有一个数据模块,专门用于数据缓存,让我们可以存储任何与dom相关的数据。原文链接源码仓库原理在我们开始学习和阅读Zepto中的数据模块之前,我们先大致了解一下dom元素和要缓存的数据是如何关联的。看看上面的图片。简单理解就是在dom元素上有一个exp(Zepto1507010934916)属性,它对应的值为整数1、2、3,data是一个大对象,存储与dom元素关联的自定义数据,类似到下面的{1:{name:'qianlongo'},2:{sex:'boy'}}DOM元素通过1,2,3数字索引关联到大对象数据。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]}你必须熟悉基本用法。需要注意的是,我们也可以直接获取以data-为前缀的html标签上定义的属性。接下来我们直接看源码实现。源代码$.fn.data=function(name,value){返回值===undefined?//通过对象设置多个值$.isPlainObject(name)?this.each(function(i,node){$.each(name,function(key,value){setData(node,key,value)})})://从第一个元素获取值(0inthis?getData(this[0],name):undefined)://给所有元素设置值this.each(function(){setData(this,name,value)})}通过上面的例子我们知道,在设置数据的时候,我们可以设置单个属性或多个属性(传递一个对象)一起设置。大量使用三目操作是Zepto一贯的风格。让我们反汇编这段代码。当value传递一个值并且不是undefined时,可以认为是设置了一个单一的数据属性。所以拿这段代码this.each(function(){setData(this,name,value)})遍历匹配的元素,调用setData方法传入元素,要设置的数据的key和value。当没有传入任何值且name是一个纯对象时。也就是说,像这样使用$box.data({baz:[1,2,3]})就是代码this.each(function(i,node){$.each(name,function(key,value)){setData(node,key,value)})})还是遍历当前匹配的元素,并遍历传入的对象名,在底层调用setData方法一一设置属性。当name不是对象时,它被认为是对数据的读取操作。这段代码(0inthis?getData(this[0],name):undefined)用于判断当前是否有匹配元素,如果有则调用getData方法,传入匹配元素集合中的第一个元素,以及要获取的数据名称属性。如果没有匹配到的元素,则直接返回undefined。整体逻辑还是很清晰的。接下来我们主要是搞清楚上面用到的函数setData和getData。并解释数据模块最初定义的变量vardata={},dataAttr=$.fn.data,camelize=$.camelCase,exp=$.expando='Zepto'+(+newDate())各个变量的解释如下/***dom中存储的数据的数据结构如下*{*1:{*name:'qianlongo',*sex:'boy'*},*2:{*age:100*}*}**dataAttr$prototype上的data方法,通过getAttribute和setAttribute设置或读取元素属性*驼峰化下划线为小驼峰函数*exp=>Zepto1507004986420设置dom上的属性,值为数据中的键1、2、3等*/setDatafunctionsetData(node,name,value){varid=node[exp]||(node[exp]=++$.uuid),store=data[id]||(data[id]=attributeData(node))if(name!==undefined)store[camelize(name)]=valuereturnstore}exp是一个类似于Zepto1507004986420的字符串,$.uuid的初始值为0,并且它会先尝试读取元素上的exp属性,如果元素没有这个属性,则为元素设置exp属性。并到数据对象中读取id(1,2,3...)属性。当然,如果不是读入data对象,首先通过调用attributeData函数属性获取所有以data-为前缀的自定义node节点,并为其赋值。既然有了自定义属性的集合,首先判断name是不是undefined,或者把name属性添加到store中。最后一次函数调用后,将返回整个数据对象存储。attributeData获取前缀为data-//从节点中读取所有“data-*”属性的集合functionattributeData(node){varstore={}$.each(node.attributes||emptyArray,function(i,attr){if(attr.name.indexOf('data-')==0)store[camelize(attr.name.replace('data-',''))]=$.zepto.deserializeValue(attr.value)})returnstore}我们先看看node.attributesmdn是什么。Element.attributes属性返回元素所有属性节点的实时集合。该集合是一个NamedNodeMap对象,不是一个数组,所以它没有数组方法,它包含的属性节点的索引顺序因浏览器而异。更准确地说,属性是字符串形式的名称/值对,每一对对应一个属性节点。示例
让$box=document.querySelector('.box')$box.dataset.age=100console.log($box.attributes)得到的数据如上图所示,接下来我们回到attributeData函数的源码分析if(attr.name.indexOf('data-')==0)store[camelize(attr.name.replace('data-',''))]=$.zepto.deserializeValue(attr.value)判断ele.attributes得到的set是否为基础ondata-开头的属性,如果是,则在store对象中添加驼峰化的属性,序列化的attr.value作为属性的值。最后,返回商店对象。getData获取与存储在data中的DOM元素关联的对象名称属性。当name属性不存在时,直接返回整个对象。functiongetData(node,name){varid=node[exp],store=id&&data[id]if(name===undefined)返回存储||setData(node)else{if(store){if(nameinstore)returnstore[name]varcamelName=camelize(name)if(camelNameinstore)返回store[camelName]}returndataAttr.call($(node),name)}}实现思路是先读取setData,在node节点上添加id,然后以id为key在数据中查找。如果不传名字,此时会直接退回整个店铺。当然如果没有找到store,会返回调用setData后返回的元素的自定义属性集合。当store存在时,首先判断store中是否存在name属性,存在则直接返回对应的属性。否则,对传入的名称进行驼峰处理,然后判断store中是否存在,存在则返回对应的属性。也就是说,如果你传入的名字是min-age或者minAge,你得到的值都是一样的。最后,如果在数据缓存中没有找到属性名,则调用dataAttr函数直接在元素上查找相关属性。removeData移除元素上绑定的数据,可以添加或更新数据,自然也可以移除数据,先看例子
let$box=$('.框')$box.data("foo",52)$box.data("bar",{myType:"test",count:40})$box.data({baz:[1,2,3]})//$box.removeData('foo')//$box.removeData('foobarbaz')//$box.removeData(['foo','bar','baz'])//$box.removeData()我们可以指定删除单个属性,或者删除以空格分隔的多个属性,或者传入一个要删除的属性数组,即使你什么都不传,元素数据上的原始设置也会清除源代码$.fn.removeData=function(names){if(typeofnames=='string')names=names.split(/\s+/)returnthis.each(function(){varid=this[exp],store=id&&data[id]if(store)$.each(names||store,function(key){deletestore[names?camelize(this):key]})})}第一遍如果传入的名字是字符串,先转成数组,然后遍历当前匹配的元素集,将元素对应的缓存数据一一删除。找到店铺后,遍历转换后的名称或店铺。如果是你指定删除的属性,先hump它,然后用delete删除。否则直接删除store中的key$.data存储任意Data到指定元素和/或返回设定值$.data=function(elem,name,value){return$(elem).data(name,value)}定义在$function上的静态方法,底层还是调用.data的Instance方法。$.hasData确定该元素是否有与之关联的Zepto数据。$.hasData=function(elem){varid=elem[exp],store=id&&data[id]返回存储?!$.isEmptyObject(store):false}定义在$function上的静态方法也是原理拿elem的id去data里面找有没有数据对象关联。如果找到并且它不是空对象,它将返回true。否则,如果没有找到或者对象为空,则返回falseremove,empty会产生一个extendedremove。使用empty方法,扩容前的remove和empty功能还在,增加了删除选中元素缓存的数据功能。;['remove','empty'].forEach(function(methodName){//在原型上缓存之前对应的remove和empty方法varorigFn=$.fn[methodName]//重写这两个方法$.fn[methodName]=function(){//获取当前选中元素的所有内部元素varelements=this.find('*')//如果是remove方法,将自己添加到获取到的元素中if(methodName==='remove')elements=elements.add(this)//调用removeData删除dom关联的数据elements.removeData()//最后调用相应的方法删除dom,或者清除dom内容returnorigFn.call(this)}})最后以上是对Zepto数据模块所有源码的分析,有问题欢迎指正。文章记录数据模块Zepto中数据缓存的原理和实现(2017-10-03)表单模块zepto表单模块源码分析(2017-10-01)这些Zepto中的实用方法集zepto模块之modules(2017-08-26)Zepto核心模块之工具与方法(2017-08-30)看zepto是如何实现DOM的增删改查(2017-10-2)为什么mouseenter和事件模块中的mouseover这么纠结?(2017-06-05)了解如何从zepto.js手动触发DOM事件(2017-06-07)谁说你只是“知道如何使用”jQuery?(2017-06-08)ajax模块原来你是这样的jsonp(原理及具体实现细节)(2017-06-11)