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;
},
评论
nihongye
2008-03-20
随手写的 没有测试过的喔,因为我那样改动后,不再需要做fn._handlers的remove工作了,所以在 destropy el的时候不需要调用这里的stopListener,因为listeners被el引用,所以去掉对el的引用,也就相应的去掉listeners的引用,再没有别的地方需要去掉(调用Ext.lib.Event是必须的)。对 stopListener的改动是为了保持原来stopListener的正确性。
----------------------------------------------------------------------------------------------------------------------------------
做了测试,按我的写法,泄漏的不行。。。
----------------------------------------------------------------------------------------------------------------------------------
做了测试,按我的写法,泄漏的不行。。。
fins
2008-03-20
nihongye 写道
怎么说呢,fn去记录_handlers,导致不容易清除_handlers,直接的想法就是让el来做这事情。觉得Ext.lib.Event的事它自己管,EventManager的事自己管,干净。
提到的Pseudo-Leaks对于一页式的ajax应用简直就是噩梦。。。
引用
提到的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做的修改不会起到作用吧???
nihongye
2008-03-20
怎么说呢,fn去记录_handlers,导致不容易清除_handlers,直接的想法就是让el来做这事情。觉得Ext.lib.Event的事它自己管,EventManager的事自己管,干净。
提到的Pseudo-Leaks对于一页式的ajax应用简直就是噩梦。。。
引用
提到的Pseudo-Leaks对于一页式的ajax应用简直就是噩梦。。。
fins
2008-03-20
不建议在 el上做 缓存
理由很简单
从ext整体的设计来看 他还是推崇 将所有的 listener 放到一处管理
也就是 Ext.lib.Event里的那个 私有的 listeners
从设计角度来讲 这样统一管理也是好的
如果每个 element单独记录自己的事件 不便于实现一套 "框架级的统一事件管理机制"
所以我觉得在el上做文章不好
理由很简单
从ext整体的设计来看 他还是推崇 将所有的 listener 放到一处管理
也就是 Ext.lib.Event里的那个 私有的 listeners
从设计角度来讲 这样统一管理也是好的
如果每个 element单独记录自己的事件 不便于实现一套 "框架级的统一事件管理机制"
所以我觉得在el上做文章不好
nihongye
2008-03-20
EventManager.js的140行开始:
假设没有全局引用el,就不会有listener泄漏之忧了吧。所以Element.js的removeAllListeners也不用改了,因为引用listen的是el本身。是否el是dom节点的话,那么ie就内存泄漏了?
//将原来的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就内存泄漏了?
fins
2008-03-20
2008-03-20更新一个临时解决方案
见主楼
见主楼
hax
2008-03-20
根据roadmap还起码要忍9个月。。。奥运都结束了。。。
sp42
2008-03-20
EXT的Observer架构源自YUI.
不过无论社区还是开发团队,已经对这个架构颇有微辞。
jack他们安排在3.0会对这项有改进:
http://extjs.com/roadmap
不过无论社区还是开发团队,已经对这个架构颇有微辞。
jack他们安排在3.0会对这项有改进:
引用
Update the Ext event registration model
http://extjs.com/roadmap
fins
2008-03-19
nihongye 写道
绑定的时候el.lisenters.push([eventName,fn])。
然后removeAllLis...的就逐一调用stop。
然后removeAllLis...的就逐一调用stop。
这个试过了 不行的 :(
事实上你仔细看一下你就会发现
不管是 listeners 还是 handlers 那么都没有挂在 el上
我想jack应该是考虑到一些潜藏的隐患了 例如循环引用
h里调fn
fn里保留 h
wfn 里调h
listeners 里保留 el eventname h wfn
如果 el 上 再保留一份 fn
... ...
想想就头大 我觉得ext的事件机制肯定可以化简的
fins
2008-03-19
我写的是伪代码
你仔细跟踪一下 肯定是一样的
你仔细跟踪一下 肯定是一样的
hax
2008-03-19
我看到2.0.2跟fins贴出来的代码有些不同啊。
hax
2008-03-19
我没用过ext,刚刚看了一下,它是有若干种adapter,可以用standalone或者用yui,jquery,prototype。evt机制似乎也是委托给这些不同的底层包的。这个bug是否是只是standalone才有呢?
nihongye
2008-03-19
绑定的时候el.lisenters.push([eventName,fn])。
然后removeAllLis...的就逐一调用stop。
然后removeAllLis...的就逐一调用stop。
hax
2008-03-19
ext的event机制似乎原先是基于YUI的?
hax
2008-03-19
btw,我下了ext2.0.2的源代码来看,其中空格和tab混用,缩进有点乱,看的我很不爽。
差沙
2008-03-19
这一问题的根本原因还是,ext要兼容其他lib的问题。
其实ext3.0的时候应该把其他的Lib统统扔掉了,不需要什么adapter,这样底层的东西就能互通了。
其实ext3.0的时候应该把其他的Lib统统扔掉了,不需要什么adapter,这样底层的东西就能互通了。
hax
2008-03-19
复杂到有点头大。感觉ext的这套东西是api演化过程中的产物,将来其实现可能会重构。
发表评论
提醒: 该博客已发表在公共论坛,博客所有留言会成为论坛回贴,留言请注意遵守论坛发贴规则
- 浏览: 706279 次
- 性别:

- 来自: 小胖儿的大城

- 详细资料
搜索本博客
我的相册
David Recordon
共 63 张
共 63 张
链接
最新评论
-
EXT 2 绚丽表格 背后的 ...
楼上的真是锐道的好员工啊 dorado整体表现确实不错 但是没有哪个单项可以用 ...
-- by fins -
EXT 2 绚丽表格 背后的 ...
http://www.bstek.com/dorado5/performance ...
-- by hotbarsmu -
[GT-Grid]列表组件 GT-Gr ...
如果一切正常 下周应该会出一个前后台结合的例子 例子已经在编写中了 不过为了 ...
-- by fins -
[GT-Grid]列表组件 GT-Gr ...
fins什么时候会有和服务端结合的版本呢?您可以给个简单的案例吗?谢谢
-- by hgq0011 -
[GT-Grid]列表组件 GT-Gr ...
这个是和ecside完全不同的产品 自然看起来也会面目全非了 呵呵
-- by fins






评论排行榜