引子
最近在做得一个项目,我是基于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属性
贴一段MDN上关于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, name, data );
},
removeData: function( elem, name ) {
return internalRemoveData( elem, name );
},
// For internal use only.
_data: function( elem, name, data ) {
return internalData( elem, name, data, true );
},
_removeData: function( elem, name ) {
return internalRemoveData( elem, name, 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()的一个小解读,也可以算是对我前段时间项目中遇到的一些小问题的记录。感谢大家阅读,如有错误,欢迎指出。
参考资料: