引子
最近在做得一个项目,我是基于reactjs来写的。项目不大不小,就带了个童鞋一起写,为了不让react写起来那么吃力,我还是引入了jquery (1.11.1)。就这样整个项目开展的还算顺利,期间踩到了一些坑,但都是react的,直到...
一切都源于这样的一个写法
_edit:(e)-> $ele = $(e.currentTarget).parents('td') _name = $ele.data('name') _filterArr = @props.items.filter (item)-> item.activityName is _name if _filterArr.length @props.onEditCallBack(_filterArr[0]) if @props.onEditCallBack e.preventDefault() return
很简单地一段coffee,获取绑在td上的data-name
,然后在items里找到name为_name的item,执行callback
可发布到线上之后,出了问题,有得item就是无法编辑,线上代码又uglify过,不好调试,这位童鞋看了半天代码也没有发现什么问题。愚安我这时候在睡午觉,迷糊中被他叫醒。
点了下页面发现,页面上一个data-name="111"
的item无法删除,看了下代码之后,拽拽的对他说:“不要乱用jquery的data,这里有缓存,大小写,类型转换三大坑,看源码去!”。然后将原来的data改为getAttribute之后,果然跑通了。为什么跑通,且看下文。事后,我也不知道当时为什么突然来了这句三大坑。既然说了,那总要跟别人讲下三个坑吧,不能打脸,不能不讲道理是吧。
先说data属性
HTML5是具有扩展性的设计,它初衷是数据应与特定的元素相关联,但不需要任何定义。data-* 属性允许我们在标准内于HTML元素中存储额外的信息,而不许需要使用类似于 classList,标准外属性,DOM额外属性或是 setUserData之类的伎俩。
一股浓浓的谷歌翻译味儿,英语好的童鞋还是去看原文,或者帮忙去翻译下,就在愚安我写这篇博客的时候,顺便提交了下翻译,连我这种大学英语考试总共有几级都不知道的人都敢翻译,何况你呢。
在外部使用JavaScript去访问这些属性的值同样非常简单。你可以使用
getAttribute()
配合它们完整的HTML名称去读取它们,但标准定义了一个更简单的方法:DOMStringMap你可以使用dataset
读取到数据。
文档里写到无论是通过getAttribute()
还是dataset
都可以轻松访问节点上得data-*
属性的值,但二者是有区别的。
getAttribute()与dataset的区别
这里补充一点儿关于DOM的小知识,直接访问节点属性和通过getAttribute访问节点属性返回的结果不一定是一样的,但getAttribute和attributes['索引']访问节点属性的结果一定是不同的(即使都访问都不存在的属性,前者返回null,后者返回undefined),举个例子
<div name="div" id="test"></div>
var div = document.getElementById('test'); div.name //undefined div.id //"test" div.getAttribute("name") //"div" div.attributes['name'] //name="div" Object.prototype.toString.call(div.attributes['name']) //"[object Attr]"
事实上,对于DOM节点而言,id与attributes是同样等级的属性。DOM不熟的同学,可以去看看这方面的资料,这里我就不跑题了。
继续看区别。
Object.prototype.toString.call(div.dataset) //"[object DOMStringMap]" Object.prototype.toString.call(div.attributes) //"[object NamedNodeMap]"
很显然,二者是不同类型的map。
div['data-a'] = 1 //1 div.getAttribute('data-a') //null div.attributes["data-a"] //undefined div.dataset["a"] //undefined //-------------------- div.setAttribute("data-foo","bar") //undefined div.getAttribute("data-foo") "bar" div.attributes["data-foo"] //data-foo="bar" div.dataset["foo"] //"bar" //-------------------- div.dataset['foo2'] = "123" //"123" div.getAttribute("data-foo2") //"123" div.attributes["data-foo2"] //data-foo2="123" div['data-foo2'] //undefined
通过以上三种方式,大家应该大致知道节点字段,节点属性,节点dataset之间的小关系与区别
再来贴一段文档
为了使用dataset对象去获取到数据属性,需要获取属性名中data-之后的部分(要注意的是破折号连接的名称需要转换为驼峰样式的名称)。
测试
div.setAttribute('data-foo-bar',123) //undefined div.dataset["fooBar"] //"123",仍为字符型 div.dataset['bar-foo'] = 123 //Uncaught DOMException: Failed to set the 'bar-foo' property on 'DOMStringMap': 'bar-foo' is not a valid property name. div.dataset["barFoo"] = 123 //123 div.getAttribute('data-bar-foo') //"123" div.dataset["barFoo"] //"123",仍为字符型
可见这里确实存在喜闻乐见的camelCase转换。
再说jquery.data的"坑"
开始翻jquery-1.11.1的源码中得data函数。
注:jquery2放弃了对一些对低版本浏览器的支持,“坑”不全,我们还是看1.X的。
jQuery.extend({ cache: {},//当设置下面这三种元素的expando属性时会抛出异常 //具体方法参见jquery的src/data/accepts下的jQuery.acceptData方法 noData: { "applet ": true,"embed ": true,// ...但是 Flash对象 (拥有classid)可以处理expando "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" },hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !isEmptyDataObject( elem ); },data: function( elem,name,data ) { return internalData( elem,data ); },removeData: function( elem,name ) { return internalRemoveData( elem,name ); },// For internal use only. _data: function( elem,data,true ); },_removeData: function( elem,true ); } });
这个就是jquery.data的大致结构,比较清晰。接下来,我们来聊聊前面说的三大坑。
类型转换坑
首先回到最开始的事故代码里,熟悉coffee
的童鞋都知道,is
关键字,在编译到javascript
时,会变成===
号(强等于),而存储在item里的name时字符型,通过$("selector").data()
函数获取文档节点的data-*
属性上的值时,调用得是jquery.fn.data
方法,这里就不贴完整代码了,贴下造成这个类型转换的部分dataAttr()
。
if ( data === undefined && elem.nodeType === 1 ) { var name = "data-" + key.replace( rmultiDash,"-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { //布尔型转换 data = data === "true" ? true : data === "false" ? false : //null型转换 data === "null" ? null : // 仅当将其转换成数字时,其字符值相对原字符值不变时,进行number型转换 +data + "" === data ? +data : //json字符串到object的转换,rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/ rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} // Make sure we set the data so it isn't changed later jQuery.data( elem,key,data ); } else { data = undefined; } }
通过我注释的部分可以很容易看出,jquery在调用jquery.data()前,会对传入的data值进行类型转换,其中转换为number的部分就是造成引子中提到到bug的原因。当然,jQuery这里完全是为了方便大家使用,我这里说采坑,纯属强行甩锅给jquery。
当然,我们上面测试过原生的javascript通过dataset
或者getAttribute
都不会做这种类型转换。
举个栗子
$(div).data("foo-bar") //123,number型 $(div).data("fooBar") //123 div.dataset["fooBar"] //"123",字符型
大小写转换坑
在上面代码中,我们注意到这么一段
//rmultiDash = /([A-Z])/g; var name = "data-" + key.replace( rmultiDash,"-$1" ).toLowerCase(); data = elem.getAttribute( name );
这里现将key中所有的大写字母前加“-”,然后统一转换为小写。
再举个栗子
var div = document.createElement('div'),key = "ID",id = 123; div.setAttribute("data-"+key,id); $(div).data(key); //undefined
当然前面也已经讲过,即使使用dataset这种结果。把这个“坑”,算在jquery的头上实在是不讲道理。不过这里,也是给像我这样比较粗心的前端童鞋,提个醒,直接写在html里的data-*
中记得要用小写,避免不必要的bug。
缓存坑
在jquery.data中核心的internalData
函数里,进行了主要的cache读写操作。我们调用$(selector).data(key,value)
的时候,进行的流程大致如下
- key,value格式化处理
- 检查elem是否有elem[internalKey],若有则作为cache中对应得id,若无则做如下处理
id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; //deletedIds默认为[]记录被铲除的id的数组 //guid是默认为1的计数器 //这样可以保证被删除的元素的id能够被放到deletedIds再利用,而不是无线递增guid造成枯竭
- 拿到id之后,检查jQuery.cache[id]是否存在,若不存在则jQuery.cache[id] = {}
- 将key为传入key的camelCase形式,value为做相应处理的value的键值对放入jQuery.cache[id]中
- 返回jQuery.cache[id]
注:internalKey = jQuery.expando = "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g,"" )
同理,调用$(selector).data(key)
时,也是现做key处理,id处理,去jQuery.cache[id]这个Object中拿到对应key的value,或返回undefined。
由于jquery这种cache机制,导致如果一个DOM节点上存在internalKey,且其刚好对应一个可以命中的cacheID,则无法通过jQuery.data()方法拿到data-*
对应的值,而是cache对应的值。
这种情形最容易在类似reactjs这种virtual-DOM在对一组元素做部分删除操作时出现。因为virtual-DOM是做增量更新,删除的virtual-DOM并不一定是将我们主观视觉上看到的那个DOM节点,而是将相邻DOM节点进行增量更新,此时虽然data-*属性仍是原来的值,但整个DOM却是那个本来已经被删除的元素,所以如果那个被删除的DOM元素曾经调用过data方法,保留了iternalKey的话,那么恭喜你,你碰到我说的缓存坑了。
当然上面这种情况,也很容易通过getAttribute("data-*")处理解决掉,不是上面大问题,无须担心。
后记
这篇blog写在愚安我离职的第二天,在星巴克坐了一下午,无聊写的,延续了我以往写东西狂贴代码凑字数的原则。可以作为jQuery.data()的一个小解读,也可以算是对我前段时间项目中遇到的一些小问题的记录。感谢大家阅读,如有错误,欢迎指出。
参考资料:
原文链接:https://www.f2er.com/react/307961.html