很多 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/
譯註,prototype 1.6 對應的程式碼,摘記如下:
function createwrapper(element, eventname, handler) { var id = geteventid(element); // 獲取繫結事件的 id var c = getwrappersforeventname(id, eventname); // 獲取對應的事件的所有回撥 if (c.pluck("handler").include(handler)) return false; // 避免重複繫結 // 新建回撥 var wrapper = function(event) { if (!event || !event.extend || (event.eventname && event.eventname != eventname)) return false; event.extend(event); handler.call(element, event); }; // 加入到回撥陣列 wrapper.handler = handler; c.push(wrapper); return wrapper; } function observe(element, eventname, handler) { element = $(element); // 對應事件的元素 var name = getdomeventname(eventname); // 事件執行方式 var wrapper = createwrapper(element, eventname, handler); // 封裝回撥 if (!wrapper) return element; // 繫結事件 if (element.addeventlistener) { element.addeventlistener(name, wrapper, false); } else { element.attachevent("on" + name, wrapper); } return element; } // 呼叫方式 document.observe("dom:loaded", function() { console.log("init: 1"); does_not_exist++; }); document.observe("dom:loaded", function() { console.log("init: 2"); });
看把 prototype 的作者給樂的 :-/
-- split --
在本人看來,原文的作者表述的技術點,除了如何建立健壯的回撥系統外,其實還有兩條。
其一,就是如何保證在出現異常的時,繼續執行期望的程式碼;其二,就是如何建立互不干擾的“執行環境”。
原文提到的 createevent 和 settimeout 都是好辦法,只是處理原作者所言在回撥系統中,的確使用 createevent 會比較合適。settimeout 相對應的詳細資訊,可移步到 realazy 兄的相關文章。
而即使出錯也能繼續執行期望的程式碼,其實可以考慮使用 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); } }
最後,留個小問題。誰知道上述的程式碼中,留言者提出的為什麼異常到最後才列印出來不?