DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> Javascript框架的自定義事件
Javascript框架的自定義事件
編輯:關於JavaScript     

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/

譯注,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);
    }
}

最後,留個小問題。誰知道上述的代碼中,留言者提出的為什麼異常到最後才打印出來不?

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved