2008-03-19
Ext2.02事件机制缺陷分析,以及解决方案 ( 3-20更新 )
2008-03-20更新一个临时解决方案.
测试发现,Ext2.02在IE下无法正常释放被删除的元素(当该元素被注册了事件时)
经过分析 发现ext事件机制中的一个bug
(
bug 具体描述见: http://fins.javaeye.com/blog/173218
测试使用工具见: http://fins.javaeye.com/blog/172891
)
使用 el.on(eventName, fn) 为el添加事件
调用 Ext.destroy(el) 方法移除el
此时,如果fn为全局类型,或者是被全局对象引用, 那么会使el元素成为孤立节点,无法彻底移除.
而如果在 Ext.destroy(el) 之前, 调用 el.un(eventName, fn) 移除添加的事件,
那么就可以彻底移除. 但是直接使用 Ext.destroy 才是ext中描述的正确做法,
切ext内部也都是这样使用的, 所以应该将解决问题的着手点放在 el.on 和 Ext.destroy方法上.
经过测试 Ext.destroy el.removeNode 均无问题. 核心问题在 事件机制. 下面详细分析一下.
==============================
产生问题的原因
执行Element.removeAllListeners时没有调用 EventManager.stopListener中的
"删除 fn._handlers 缓存内的相关数据 "
导致在IE下 当 fn 为全局对象 或者是被引用时, 元素无法被正确移除.
-----------------------------------------
如果只是简单的修改 Element.removeAllListeners
让其 调用 Ext.Event.un 时 改成调用 EventManager.stopListener 是不行的
因为 Element.removeAllListeners 调用 Ext.Event.un 时 ,传递的函数参数是h, 而不是最初的fn
但是 EventManager.stopListener需要得到 最初的fn.
-----------------------------------------
现在的情况是 从 fn 能找到h (fn._handlers) ,但是 通过h无法找到fn
缓存Ext.Event._listeners 中也没有存放 最初的fn.
-----------------------------------------
也许可以考虑在 removeAllListeners 或 purgeElement 中对 fn._handlers 进行清除,但是 拿不到 最初的fn
-----------------------------------------
如果之前 强制 做一个引用, 例如 h._core =fn;
然后在 Element.removeAllListeners 加以利用 利用完之后 再清除, 似乎看起来不错
但是我试了 ,失败 !!!!
具体原因我也说不清
==================================
我觉得 如果要解决 这个bug 确实要对ext的整个事件机制做一番大改动.(恕我直言,ext的这套事件机制真的有点太.... )
以上是我最近研究的成果
发上来和大家分享,如果说的不对 请务必一定马上纠正我, 以免误人子弟 谢谢大家了
======================================
下面附上刚刚写出的解决方案,请大家拍砖, 我想肯定还有更好的方法.
第一步 ========================
EventManager.js 153行
第二步 ========================
ext-base.js 227行
测试发现,Ext2.02在IE下无法正常释放被删除的元素(当该元素被注册了事件时)
经过分析 发现ext事件机制中的一个bug
(
bug 具体描述见: http://fins.javaeye.com/blog/173218
测试使用工具见: http://fins.javaeye.com/blog/172891
)
使用 el.on(eventName, fn) 为el添加事件
调用 Ext.destroy(el) 方法移除el
此时,如果fn为全局类型,或者是被全局对象引用, 那么会使el元素成为孤立节点,无法彻底移除.
而如果在 Ext.destroy(el) 之前, 调用 el.un(eventName, fn) 移除添加的事件,
那么就可以彻底移除. 但是直接使用 Ext.destroy 才是ext中描述的正确做法,
切ext内部也都是这样使用的, 所以应该将解决问题的着手点放在 el.on 和 Ext.destroy方法上.
=============================
销毁元素的方法(很简单)
=============================
Ext.destroy(el){
el.removeAllListeners();
el.removeNode();
}
经过测试 Ext.destroy el.removeNode 均无问题. 核心问题在 事件机制. 下面详细分析一下.
=============================
给一个元素添加事件
=============================
Element.on(eventName, fn) {
el=this;
调用 EventManager.on( el,eventName, fn ){
调用 EventManager.listener( el,eventName, fn ){
包装 h <---- fn
缓存 fn._handlers <---- [ [h] ]
调用 Ext.Event.on( el,eventName, h ) {
包装 wfn <---- h
缓存 Ext.Event._listeners <---- [ el , eventName, h, wfn ]
el.addEvent( wfn )
}
}
}
}
注意:真正注册到el上的事件是wfn
=============================
移除一个元素的事件
=============================
Element.un(eventName, fn) {
el=this;
调用 EventManager.un( el,eventName, fn ){
调用 EventManager.stopListener( el,eventName, fn ){
取得之前缓存的 h <---- fn._handlers
删除 fn._handlers 缓存内的相关数据
调用 Ext.Event.un( el,eventName, h ) {
取得之前缓存的 wfn <---- Ext.Event._listeners
el.removeEvent( wfn )
删除 Ext.Event._listeners 缓存内的相关数据
}
}
}
}
=============================
移除一个元素的所有注册的事件
=============================
Element.removeAllListeners() {
el=this;
调用 Ext.Event.purgeElement(el){
取得缓存中所有的和el相关的信息 l[] <---- Ext.Event._listeners
<循环开始 l[] >
从 l中取得 eventName <---- l[i];
从 l中取得 h <---- l[i];
调用 Ext.Event.un( el,eventName, h ) {
取得之前缓存的 wfn <---- Ext.Event._listeners
el.remove( wfn )
删除 Ext.Event._listeners 缓存内的相关数据
}
<循环结束>
}
}
==============================
产生问题的原因
执行Element.removeAllListeners时没有调用 EventManager.stopListener中的
"删除 fn._handlers 缓存内的相关数据 "
导致在IE下 当 fn 为全局对象 或者是被引用时, 元素无法被正确移除.
-----------------------------------------
如果只是简单的修改 Element.removeAllListeners
让其 调用 Ext.Event.un 时 改成调用 EventManager.stopListener 是不行的
因为 Element.removeAllListeners 调用 Ext.Event.un 时 ,传递的函数参数是h, 而不是最初的fn
但是 EventManager.stopListener需要得到 最初的fn.
-----------------------------------------
现在的情况是 从 fn 能找到h (fn._handlers) ,但是 通过h无法找到fn
缓存Ext.Event._listeners 中也没有存放 最初的fn.
-----------------------------------------
也许可以考虑在 removeAllListeners 或 purgeElement 中对 fn._handlers 进行清除,但是 拿不到 最初的fn
-----------------------------------------
如果之前 强制 做一个引用, 例如 h._core =fn;
然后在 Element.removeAllListeners 加以利用 利用完之后 再清除, 似乎看起来不错
但是我试了 ,失败 !!!!
具体原因我也说不清
==================================
我觉得 如果要解决 这个bug 确实要对ext的整个事件机制做一番大改动.(恕我直言,ext的这套事件机制真的有点太.... )
以上是我最近研究的成果
发上来和大家分享,如果说的不对 请务必一定马上纠正我, 以免误人子弟 谢谢大家了
======================================
下面附上刚刚写出的解决方案,请大家拍砖, 我想肯定还有更好的方法.
第一步 ========================
EventManager.js 153行
//修改 Ext.EventManager的 私有方法 listen // E.on(el, ename, h); // 改为如下 (即,多传一个最初的 fn) E.on(el, ename, h , fn);
第二步 ========================
ext-base.js 227行
//修改 Ext.lib.Event 的 addListener 和 removeListener 方法
addListener: function(el, eventName, fn , ofn) {
el = Ext.getDom(el);
if (!el || !fn) {
return false;
}
if ("unload" == eventName) {
unloadListeners[unloadListeners.length] =
[el, eventName, fn];
return true;
}
// prevent unload errors with simple check
var wrappedFn = function(e) {
return typeof Ext != 'undefined' ? fn(Ext.lib.Event.getEvent(e)) : false;
};
var li = [el, eventName, fn, wrappedFn,ofn];
var index = listeners.length;
listeners[index] = li;
this.doAdd(el, eventName, wrappedFn, false);
return true;
},
removeListener: function(el, eventName, fn) {
var i, len;
el = Ext.getDom(el);
if(!fn) {
return this.purgeElement(el, false, eventName);
}
if ("unload" == eventName) {
for (i = 0,len = unloadListeners.length; i < len; i++) {
var li = unloadListeners[i];
if (li &&
li[0] == el &&
li[1] == eventName &&
li[2] == fn) {
unloadListeners.splice(i, 1);
return true;
}
}
return false;
}
var cacheItem = null;
var index = arguments[3];
if ("undefined" == typeof index) {
index = this._getCacheIndex(el, eventName, fn);
}
if (index >= 0) {
cacheItem = listeners[index];
}
if (!el || !cacheItem) {
return false;
}
this.doRemove(el, eventName, cacheItem[this.WFN], false);
fn=listeners[index][4];
if (fn){
var id = Ext.id(el), hds = fn._handlers, hd = fn;
if(hds){
for(var i = 0, len = hds.length; i < len; i++){
var h = hds[i];
if(h[0] == id && h[1] == eventName){
hd = h[2];
hds.splice(i, 1);
break;
}
}
}
}
delete listeners[index][this.WFN];
delete listeners[index][this.FN];
listeners.splice(index, 1);
return true;
},


评论
----------------------------------------------------------------------------------------------------------------------------------
做了测试,按我的写法,泄漏的不行。。。
提到的Pseudo-Leaks对于一页式的ajax应用简直就是噩梦。。。
这个fn 不是只属于一个el的 更不是el自己的
我来详细阐述一下我的观点:
一个事件里的几个核心角色:
元素(el) 事件名(eventName) 原始函数(fn) 被包装函数(h) 二次包装函数(wfn)
这些东西的地位是一样重要的 存放在一起应该更合适.
所以我还是觉得对 现有的 listeners 做扩展 让他多装一个元素比较合适.
事实上,我觉得ext的 fn._handlers 的做法也并不好
首先他改变了原始函数, 这个做法实际上很欠妥当
另外一个也是违背了我上面提到的统一管理的原则.
他这么做无非是为了达到下面的几个目的:
1 可以根据一个fn得到包装后的 h
2 可以知道一个fn 被注册到了哪些元素上
3 可以知道一个fn 都被哪些事件类型调用
如果他能够像我刚刚的做法 ,对 listeners 做一个扩充, 把 fn也加进去
那么根本就不需要这个蹩脚的 fn._handlers 了
你觉得 这个 fn._handlers 是一个好的设计吗?
如果你也觉得这个设计不好 那么 去掉fn._handlers 后,按照你的设计 el上缓存 fn
我怎么能够达到 上面提到的三个目的呢?
综上所述 我还是推崇 "统一存放 统一管理"
为了更好的对整个事件层进行统一的管理和控制,将事件各个核心角色统一缓存比较好,
而不要每个元素自己来缓存自己的事件信息.
关于设计的问题 一向很难统一, 一个软件可能有多种设计是合理的 也可能任何设计都不是很合理的.
所以关于设计的问题就不争论了.
===============================
我更关心的是结果 我想问一下 你的代码你测试过吗? 确实有效吗
因为我前面提过 在 destroy时 根本不会调用 stopListening方法
你对stopListening做的修改不会起到作用吧???
提到的Pseudo-Leaks对于一页式的ajax应用简直就是噩梦。。。
理由很简单
从ext整体的设计来看 他还是推崇 将所有的 listener 放到一处管理
也就是 Ext.lib.Event里的那个 私有的 listeners
从设计角度来讲 这样统一管理也是好的
如果每个 element单独记录自己的事件 不便于实现一套 "框架级的统一事件管理机制"
所以我觉得在el上做文章不好
//将原来的fn_handlers去掉,增加 el._listeners = el._listeners || []; el._listeners.push([ename, fn,h]); E.on(el, ename, h); if(ename == "mousewheel" && el.addEventListener){ // workaround for jQuery el.addEventListener("DOMMouseScroll", h, false); E.on(window, 'unload', function(){ el.removeEventListener("DOMMouseScroll", h, false); }); } if(ename == "mousedown" && el == document){ // fix stopped mousedowns on the document Ext.EventManager.stoppedMouseDownEvent.addListener(h); } return h; }; var stopListening = function(el, ename, fn){ var hds = el._listeners, hd = fn; if(hds){ for(var i = 0, len = hds.length; i < len; i++){ var h = hds[i]; if(h[0] == ename && h[1] == fn){//无需Id,增加h[1]==fn的判断 hd = h[2]; hds.splice(i, 1); break; } } } E.un(el, ename, hd);假设没有全局引用el,就不会有listener泄漏之忧了吧。所以Element.js的removeAllListeners也不用改了,因为引用listen的是el本身。是否el是dom节点的话,那么ie就内存泄漏了?
见主楼
不过无论社区还是开发团队,已经对这个架构颇有微辞。
jack他们安排在3.0会对这项有改进:
http://extjs.com/roadmap
然后removeAllLis...的就逐一调用stop。
这个试过了 不行的 :(
事实上你仔细看一下你就会发现
不管是 listeners 还是 handlers 那么都没有挂在 el上
我想jack应该是考虑到一些潜藏的隐患了 例如循环引用
h里调fn
fn里保留 h
wfn 里调h
listeners 里保留 el eventname h wfn
如果 el 上 再保留一份 fn
... ...
想想就头大 我觉得ext的事件机制肯定可以化简的
你仔细跟踪一下 肯定是一样的
然后removeAllLis...的就逐一调用stop。
其实ext3.0的时候应该把其他的Lib统统扔掉了,不需要什么adapter,这样底层的东西就能互通了。