从开发不是看到的那么简单继续讨论:
现在用 jQuery 的人不太多了吧,我这个三脚猫(鼠)还是挺推荐新人用的,毕竟可以非常简单的先玩起来,实现功能,获得成就感,产生兴趣,然后正向循环。
不过现在 JQ 确实有点跟不上这个时代了,当页面中有大量数据需要处理的时候,它有点不方便。
过去也有那么一阵子,大家反对 JQ,认为其实并没有必要使用它,并给出了每个功能的替代语法,看起来都很简单,我把这些替代语法收藏了好久好久……
一、缘起
最近在写阅读模式的脚本嘛,就要在页面中添加一个界面,当关闭阅读模式的时候要移除这个页面(其实是内容部分,这里简化模型)。这里就开始有问题了,移除了大量的元素,但是某一些元素上我可能绑定了事件,如果只是单纯的移除元素,而不解绑事件,如果依然存在相关引用,则这个元素和事件还是会遗留在内存之中的,这是一种内存泄漏。
当然,现在大家设备的内存都挺多的。阅读模式在一个页面下开关十几次的可能性已经非常小了,而这种内存泄漏大概起码需要几百次开关之后才能积累到产生明显影响,显然,在这之前,这个页面早已经被刷新、跳转或者关闭了。所以这大概也不算一个问题。
但,我强迫症嘛。而且抬杠涨学问,又没有 deadline 在身后,那就完美主义一下吧。
每次添加事件监听器,同时注册一个移除事件的函数,在需要的时候调用,很合理,但是使用多了就有点麻烦。
二、思考
被 @w568w 提醒之后,我开始考虑能不能把这个事情弄得简单一点。
原本的计划中是等这件事情让我觉得忍无可忍的时候整理一下,把相关的代码合并成一个方法(现在看来最后我还是会这样做,不过这个行动可能提前了)。
然后搜索有没有办法简单的移除元素上的所有事件,发现 JQ 的 remove 和 empty 方法可以,而且是移除某个元素(或者所有后代)所有内容的同时移除其中所有元素上绑定的事件。果然,我遇到的问题都不是新问题。
就想知道他是怎么实现的,但是在去查询源码之前,我去看了看最初提到的那些替代代码,真好,真简单,替代 empty 的代码是:innerHTML = ''
,这真就绝了!
三、学习
我们来看看 empty 的源码吧:
empty: function() {
var elem,
i = 0;
for ( ; ( elem = this[ i ] ) != null; i++ ) {
if ( elem.nodeType === 1 ) {
// Prevent memory leaks
jQuery.cleanData( getAll( elem, false ) );
// Remove any remaining nodes
elem.textContent = "";
}
}
return this;
}
Prevent memory leaks 翻译成中文:“防止内存泄漏”。上面的替代方式我真的是十分呵呵哒了!
很多对于 empty 的源码解析基本到这一步,后面虽然有所解读,但是没有写出其引用的代码,那么我们继续吧。
四、清理
getAll 函数暂时略过,目测关键问题在 cleanData 这里,上源码(我直接在上面标注了,中文注释是我写的):
cleanData: function( elems ) {
/**
* 目测是记录了一些特殊事件,难道后面遍历已知的各种事件名称?
* 但是对应的事件函数怎么获得呢?吊起了胃口
*/
var data, elem, type,
special = jQuery.event.special,
i = 0;
/**
* 这个循环条件写的简练漂亮啊,学习了
* 最近写适配旧设备的代码,for 循环写的我想吐,能简练点是极好的
*/
for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
/**
* acceptData 是个简单判断元素类型的方法
*/
if ( acceptData( elem ) ) {
/**
* dataPriv.expando 是什么?
* this.expando = jQuery.expando + Data.uid++;
* jQuery.expando 是什么?
* // Unique for each copy of jQuery on the page
* expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
*
* 总之,这里得出的是一个标记,而这个标记是 JQ 自己搞出来的
* elem 指的是元素,元素通过标记获得数据(data),里面有事件的相关数据
* 这里感觉和我对于注册移除事件的管理思路已经类似了
* 下一个问题:elem 是怎样的结构?
* 回归 getAll 函数
*/
if ( ( data = elem[ dataPriv.expando ] ) ) {
if ( data.events ) {
for ( type in data.events ) {
if ( special[ type ] ) {
jQuery.event.remove( elem, type );
// This is a shortcut to avoid jQuery.event.remove's overhead
} else {
jQuery.removeEvent( elem, type, data.handle );
}
}
}
// Support: Chrome <=35 - 45+
// Assign undefined instead of using delete, see Data#remove
elem[ dataPriv.expando ] = undefined;
}
if ( elem[ dataUser.expando ] ) {
// Support: Chrome <=35 - 45+
// Assign undefined instead of using delete, see Data#remove
elem[ dataUser.expando ] = undefined;
}
}
}
}
五、获取
直接上代码:
function getAll(context, tag) {
// Support: IE <=9 - 11+
// Use typeof to avoid zero-argument method invocation on host objects (trac-15151)
var ret;
if (typeof context.getElementsByTagName !== "undefined") {
ret = context.getElementsByTagName(tag || "*");
} else if (typeof context.querySelectorAll !== "undefined") {
ret = context.querySelectorAll(tag || "*");
} else {
ret = [];
}
if (tag === undefined || (tag && nodeName(context, tag))) {
return jQuery.merge([context], ret);
}
return ret;
}
但没什么特别的,就是获取所有符合标签的后代元素,如果 context 本身符合条件,也合并入结果之中。getElementsByTagName
和 querySelectorAll
都在说明这就是普通的元素。
但,最初传入的参数是什么?
getAll( elem, false )
再往上倒:
elem = this[ i ]
如果 this === document.body.querySelectorAll('p')
之类,那就到此结案了。但是,显然应该是 this === $('p')
这样,所以那不是单纯的元素,而是一个被 JQ 封装(代理?名词这方面我是渣渣)的对象。
六、另一条线
我又去查找 data.events
,找到了 jquery/src/event.js
这里,有事件添加、移除等函数,代码太长,不贴了。
不过应该可以粗略的出结果——JQ 也是记录每一个事件触发器,然后在需要的时候进行移除的,并没有用到我不了解的高级方法。
如此,考证几个小时,上午过去了,准备吃午饭。代码……什么代码,进度啥的,别问,问就是在写了,在写了(逃