Dean Edwards 最近 有篇文章 很精彩,忍不住在这里翻译下。
-- Split --
很多 Javascript 框架都提供了自定义事件(custom events),例如 jQuery、YUI 以及 Dojo 都支持「document ready」事件。而部分自定义事件是源自回调(callback)。
回调将多个事件句柄存储在数组中,当满足触发条件时,回调系统则会从数组中获取对应的句柄并执行。那么,这会有什么陷阱呢?在回答这个问题之前,我们先看下代码。
下面是两段代码依次绑定到 DOMContentLoaded
事件中
document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // 这里会抛出异常
}, false);
document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 2");
}, false);
那么运行这段代码会返回什么信息?显然,会看见这些(或者类似的):
Init: 1
Error: DOES_NOT_EXIST is not defined
Init: 2
可以看出,两段函数都被执行。即使第一个函数抛出了个异常,但并不影响第二段代码运行。
麻烦
OK,我们回来看下常见框架中的回调系统。首先,我们看下 jQuery 的(因为它很流行):
$(document).ready(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // 这里会抛出异常
});
$(document).ready(function() {
console.log("Init: 2");
});
然后控制台中输出了什么?
Init: 1
Error: DOES_NOT_EXIST is not defined
这样问题就很明了了。回调系统其实很脆弱 -- 如果中间有段代码抛出了异常,那么其余将不会被执行。想象下在实际情况中,这后果可能会更严重,譬如有些糟糕的插件可能会「一粒老屎坏了一锅粥」。
其他的框架,Dojo 的情况和 jQuery 类似,不过 YUI 的情况有些许不同。在它的回调系统中,使用了 try/catch 语句避免因异常发生的中断。但有个小小的负面影响,就是看不到相应的异常了。
YAHOO.util.Event.onDOMReady(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // 这里会抛出异常
});
YAHOO.util.Event.onDOMReady(function() {
console.log("Init: 2");
});
输出:
Init: 1
Init: 2
那么,有无完美的解决方案呢?
解决方案
我想到了个解决方案,就是将回调和事件结合起来。可以先建立个事件,当回调触发时才运行它。由于每个事件都有其独立的运行环境(execution context),那么即使其中某个事件抛出了异常将不会影响其他的回调。
这听起来有点复杂,还是代码说话吧。
var currentHandler;
// 标准事件支持
if (document.addEventListener) {
document.addEventListener("fakeEvents", function() {
// 执行回调
currentHandler();
}, false);
// 新建事件
var dispatchFakeEvent = function() {
var fakeEvent = document.createEvent("UIEvents");
fakeEvent.initEvent("fakeEvents", false, false);
document.dispatchEvent(fakeEvent);
};
} else {
// 针对 IE 的代码在后面详细阐述
}
var onLoadHandlers = [];
// 将回调加入数组中
function addOnLoad(handler) {
onLoadHandlers.push(handler);
};
// 逐条取出回调,并利用上述新建的事件执行
onload = function() {
for (var i = 0; i < onLoadHandlers.length; i++) {
currentHandler = onLoadHandlers[i];
dispatchFakeEvent();
}
};
万事俱备,让我们将上面坨代码扔到我们新的回调系统中
addOnLoad(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // 这里会抛出异常
});
addOnLoad(function() {
console.log("Init: 2");
});
上帝保佑,看运行结果我们看到了如下的信息:
Init: 1
Error: DOES_NOT_EXIST is not defined
Init: 2
赞!这就是我们期望的。这两个回调都运行而且互不影响,并且还能获得异常的信息,太好了!
好了,我们回过头来扶起 Internet Explorer 这个「阿斗」(我已经听见场下观众的建议了)。Internet Explorer 不支持 W3C 的标准事件规范,谢天谢地好在它有自身的实现 -- 有个 fireEvents 的方法,但只能在用户事件的时候触发(例如用户点击 click)。
不过终于找到了门道,我们来看下具体代码:
var currentHandler;
if (document.addEventListener) {
// 省略上述的代码
} else if (document.attachEvent) { // MSIE
// 利用扩展属性,当此对象被改变时触发
document.documentElement.fakeEvents = 0;
document.documentElement.attachEvent("onpropertychange", function(event) {
if (event.propertyName == "fakeEvents") {
// 执行回调
currentHandler();
}
});
dispatchFakeEvent = function(handler) {
// 触发 propertychange 事件
document.documentElement.fakeEvents++;
};
}
简而言之,殊途同归,只是针对 Internet Explorer 使用了 propertychange
事件作为触发器。
更新
有些用户留言建议使用 setTimeout
:
try { callback(); } catch(e){ setTimeout(function(){ throw e; }, 0); }
而下面是我的考虑
如没特别的要求,其实定时器的确也能搞定这问题。
上面仅仅是举例说明了这一技术的可行性。
意义在于,目前很多框架在回调系统的实现都非常的
脆弱,这或许能给这些框架能它们提供更优化的思路。
而定时器的实现并非实际的触发了事件,在实际事件
中,事件会被顺序的执行、可相互影响(譬如冒泡)、
还可以停止 -- 而这些是定时器无法做到的。
总之,最重要的是已经实现了包括 Internet Explorer 在内,使用事件执行回调的实现。如果你正编写基于事件代理的回调系统,我想你会对这一技术感兴趣的。
更新2
Prototype 在针对 Internet Explorer 的自定义事件处理上,也是同上述的方法触发回调:
http://andrewdupont.net/2009/03/24/link-dean-edwards/ 。
而即使出错也能继续运行期望的代码,其实可以考虑使用 finally 语句 ,下面是个例子:
var callbacks = [
function() { console.log(0); },
function() { console.log(1); throw new Error; },
function() { console.log(2); },
function() { console.log(3); }
];
for(var i = 0, len = callbacks.length; i < len; i++) {
try {
callbacks[i]();
} catch(e) {
console.info(e); // 获得异常信息
} finally {
continue;
}
}
这一灵感同样来自 Dean Edwards 文章后的回复 ,在这里也贴下吧:
function iterate(callbacks, length, i) {
if (i >= length) return;
try {
callbacks[i]();
} catch(e) {
throw e;
} finally {
iterate(callbacks, length, i+1);
}
}
最后,留个小问题。谁知道上述的代码中,留言者提出的为什么异常到最后才打印出来不?
这篇文章我也是在 Google reader 里加了星等有空的时候看的,呵呵。
关于你最后的问题,我试试……
MDC的文档里(/en/Core_JavaScript_1.5_Reference/Statements/try...catch):
The finally clause executes after the try block and catch clause(s) execute but before the statements following the try statement.
这句话我看有点含糊,但无论如何那个递归调用是在catch block的"throw e;"语句执行之前执行的,所以异常到最后才被捕捉到。
该留言者后来又留了一条解释说“The above obviously only throws the first error… and stops further processing.”不知道你看见了没有,呵呵。
他说的这个似乎有点问题,应该说是 only throws the last error 更易懂一点。因为finally里那个递归调用,导致最后抛出的error被最先 rethrow 出来,程序终止,先 catch 到的那些error的rethrow都没有进行。
忘了说,那个 finally {continue;} 确实很棒……
我又想了想,我引用MDC的那段话似乎对这个问题没啥意义,人家说的也并不含糊。
语言只是保证finally块总是能够执行,这一点决定了在这个单线程的情景中,异常最后被打印(被外部container捕捉到)。
您好!昨天跟您发邮件了,不知道您收到没有。
关于您写的代码,有些地方不懂的想直接跟您交流,请教您一下!
在E262中有下面一段话
Evaluate Block.Let C = Result(1).If Result(1).type is not throw, go to step 6.Evaluate Catch with parameter Result(1).If Result(4).type is not normal, Let C = Result(4).Evaluate Finally.If Result(6).type is normal, return C.Return Result(6).The production TryStatement : try Block Catch Finally is evaluated as follows:
首先是5 ,如果执行catch里面的表达式不正常的话,而我们throw e,显然不正常,所以我们的C应该被重置,而此时C就是e,然后到7,如果我们执行6返回的结果正常的话,返回C,所以当我们需要返回这个错误的C的时候,自然就会报错了。
iterate(callbacks, length, i+1);但是为什么会最后报这个错呢?是因为我们执行的时候并不会返回,因为存在递归,当执行完最后一个函数后,然后我们的递归才开始一个一个返回。
我们可以对代码进行一下修改
} finally {
console.log('f--->'+i);
}然后把throw e 从第二个移到最后一个callback函数中,就会看到了,当我们打印出f---3后,然后就抛出了异常,说明打印3完后,就需要返回值了,那我们返回一个e,当然就报错了呀!我们还可以把我们的callback函数中的。throw e的位置移到原来的位置,就可以看出当我们到f---1后,才报错。
不知道解释清楚了没有呀!但是意思应该差不多,就是1,2,3,4,5,6,7这几句话的理解呀!
我也很喜欢这个finally {continue;}
看了半天没看懂ls几位的的“很喜欢这个finally{continue;}”~
恕我才疏学浅,下面这段代码
var callbacks = [
function() { console.log(0); },
function() { console.log(1); throw new Error; },
function() { console.log(2); },
function() { console.log(3); }
];
for(var i = 0, len = callbacks.length; i < len; i++) {
try { callbacks[i](); } catch(e) { console.info(e); // 获得异常信息 } finally { continue; }}
if(i % 2 === 1){ throw i; }把continue放在finally中还是try语句外面有区别吗???
异常已经被捕获,并且没有再抛出了,也就是说这个异常不会继续扩散。
下面的例子说明了这一点:
function doSomething(i){
}
for (var i = 0; i < 10; i++) { try { doSomething(i); } catch (e) { throw e; } finally { continue; } }function runTest(){
}
runTest();try{
}catch(e){
alert(e);// 这里根本不会被调用
}