zoukankan      html  css  js  c++  java
  • Javascript框架的自定义事件(转)

    很多 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);
        }
    }

    最后,留个小问题。谁知道上述的代码中,留言者提出的为什么异常到最后才打印出来不?

  • 相关阅读:
    省队集训Ⅱ-Day5
    省队集训Ⅱ-Day4
    省队集训Ⅱ-Day3
    省队集训Ⅱ-Day2
    省队集训Ⅱ-Day1
    并查集 Pro
    树上带修: 莫队Ⅳ
    树上骗分: 莫队Ⅲ
    带修骗分: 莫队Ⅱ
    骗分带师: 莫队Ⅰ
  • 原文地址:https://www.cnblogs.com/jenry/p/3733426.html
Copyright © 2011-2022 走看看