無標題文檔

回调和事件(翻译)

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

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

利用 DOM 特性的两个小技巧

此类技巧还有很多,欢迎继续分享

解析 URL

从 James Padolsey 的 Blog 中看到的个小技巧,就是利用 a 标签的 DOM 属性解析 URL 字符串。

// This function creates a new anchor element and uses location
// properties (inherent) to get the desired URL data. Some String
// operations are used (to normalize results across browsers).
 
function parseURL(url) {
    var a =  document.createElement('a');
    a.href = url;
    return {
        source: url,
        protocol: a.protocol.replace(':',''),
        host: a.hostname,
        port: a.port,
        query: a.search,
        params: (function(){
            var ret = {},
                seg = a.search.replace(/^\?/,'').split('&'),
                len = seg.length, i = 0, s;
            for (;i<len;i++) {
                if (!seg[i]) { continue; }
                s = seg[i].split('=');
                ret[s[0]] = s[1];
            }
            return ret;
        })(),
        file: (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
        hash: a.hash.replace('#',''),
        path: a.pathname.replace(/^([^\/])/,'/$1'),
        relative: (a.href.match(/tp:\/\/[^\/]+(.+)/) || [,''])[1],
        segments: a.pathname.replace(/^\//,'').split('/')
    };
}

可对比下「传统的」正则解析方式( 取自 Tbra 库 ),至少上面代码看起来更容易理解得多

parseURL: (function() {
    var keys = ['source', 
                'prePath', 
                'scheme', 
                'username', 
                'password', 
                'host', 
                'port', 
                'path', 
                'dir', 
                'file',
                'query', 
                'fragment'];
    var re = /^((?:([^:\/?#.]+):)?(?:\/\/)?(?:([^:@]*):?([^:@]*)?@)?([^:\/?#]*)(?::(\d*))?) \
               ((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*)) \
               (?:\?([^#]*))?(?:#(.*))?/;    
    return function(sourceUri) {
        var uri = {};
        var uriParts = re.exec(sourceUri);
        for(var i = 0; i < uriParts.length; ++i) {
            uri[keys[i]] = (uriParts[i] ? uriParts[i] : '');
        }
        return uri;
    }
})();

(反)转义 HTML

取自 Prototype 中的相应代码

escapeHTML: function(str) {
    var div  = document.createElement('div');
    var text = document.createTextNode(str);
    div.appendChild(text);
    return div.innerHTML;
}

unescapeHTML: function(str) {
    var div       = document.createElement('div');
    div.innerHTML = str.replace(/<\/?[^>]+>/gi, '');
    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
}

-- EOF --

动画初探(实现)

说完了理论 ,我们来做点实事。这篇文章将介绍使用 Javascript 实现的动画组件。

下面记录下当时编写这个组件的考虑的些问题,对技术细节感兴趣的朋友,可以 到这里参看源代码

  1. 需要几个回调函数控制初始化、动画运行时、动画完成的情况
  2. 将动画精确到运行了几帧
  3. 使用 setInterval 还是 setTimeout,最后还是选择了 setInterval( 详细
  4. 不管元素需要更改那些属性,提供相应公式即可,但这可能带来性能问题
  5. 要可扩展、而且稳定

这个动画组件的用法简要的说明下,首先我们要确定元素从哪里移动(begin)到哪里(final),我们可以将两个信息绑定到一起,比如

var position = {from: 0, to: 100};

然后实例化个动画组件,指定动画的运行时长(duration)以及动画运行类型(tween)

var motion = new Motion(duration, tween);

另外,我们当然希望在动画初始化、开始以及进行和结束时候做额外的事情,那么加入相应 的回调:

// 初始化
motion.onInit = function() {
    // ...
};

// 开始
motion.onStart = function() {
    // ...
};

// 动画运行时
motion.onTweening = function() {
    // 需要获取某个时间点的坐标时,可以使用 this.equation 方法
    var p = this.equation(position.from, position.to);
};

// 动画完成时
motion.onComplete = function() {
    // ...
};

当一切准备就绪,那么就可以开始动画了

motion.start();

当然,任何时候我们都希望能停止动画,那么就

motion.stop();

那么我们就可以通过这个动画组件完成常见的动画效果了。 这里有个 DEMO ,看下这个动画组件的实际使用例子。按照传统,代码可以在这里 打包下载

附,参考资源: YUI 的动画组件mootools 的动画组件

-- 全文完 --

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHubTwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 要知道作为码农取名是件很难的事情,所以不想在取名这事情上太费心思。

作为八零后,自认为还仅存点点可能不怎么被理解的幽默感,以及对平淡生活的追求和向往。 为了避免不必要的麻烦,声明本站所输出的内容以及观点仅代表个人,不代表自己所服务公司或组织的任何立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

分类

搜索

文章