React.js 下的 $.data() "踩坑"实录

引子

最近在做得一个项目,我是基于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()的一个小解读,也可以算是对我前段时间项目中遇到的一些小问题的记录。感谢大家阅读,如有错误,欢迎指出。

作者博客原文地址

参考资料:

  1. https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/Using_data_att…
  2. https://github.com/jquery/jquery/blob/1.11-stable/src/data.js
  3. http://blog.rx836.tw/blog/jquery-data-cache/
    原文作者:愚安
    原文地址: https://segmentfault.com/a/1190000002696570
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞