Callbacks vs Events
前言:本文翻译自Dean Edwards的一篇文章,原文地址:http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/。
文章主要指出了用“回调模式实现自定义事件”的一些弊端,同时提出了一种解决方案,即将回调的函数包装成原生事件,利用事件系统触发
来完成回调的触发。
大多数主流的js库都声称他们支持一种或多种形式的自定义事件。比如,jQuery,YUI以及Dojo他们都支持自定义事件“document ready”。然而
这些自定义事件的实现往往使用的是一种回调模式。
回调系统(模式)往往需要一个数组来存储回调函数。如果当前的事件被处罚,则回调系统会轮询这个数组,并依次调用这些回调函数。听起来这样
实现是可以的,究竟会出现什么问题呢?在我回答之前,先看看例子。
下面是一个简单的例子,使用了DOMContentLoaded事件来完成两个相互独立的初始化:
document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
}, false); document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 2");
}, false);
你认为当事件出发时会出现什么效果呢?
是这样的:
Init: 1 Error: DOES_NOT_EXIST is not defined Init: 2
关键在于,这两个函数都执行了,第一个函数在执行时抛出错误,但并不影响第二个函数的执行。
问题所在
现在我们尝试下用“回调模式”实现自定义事件的系统。在这里,使用jQuery库。
$(document).ready(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
}); $(document).ready(function() {
console.log("Init: 2");
});
我们会在console中看到如下结果:
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++; // this will throw an error
}); YAHOO.util.Event.onDOMReady(function() {
console.log("Init: 2");
});
结果:
Init: 1 Init: 2
确实如此,你看不到第一个回调的错误。
但是,我们需要寻找真正的解决(兼容)方案。
解决方案
可以将回调模式和真实事件触发结合在一起混合使用。我们可以出发一个伪事件,并在该事件内,执行回调函数。每个回调函数都拥有其自己的执行上下文。如果在伪事件中出现错误(译者注:什么意思?当伪事件的回调函数出现错误?)也不会影响我们的回调系统。
这听起来可能有些复杂,我会用例子来解释它:
var currentHandler; if (document.addEventListener) {
document.addEventListener("fakeEvents", function() {
// execute the callback
currentHandler();
}, false); var dispatchFakeEvent = function() {
var fakeEvent = document.createEvent("UIEvents");
fakeEvent.initEvent("fakeEvents", false, false);
document.dispatchEvent(fakeEvent);
};
} else { // MSIE // I'll show this code later
} 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++; // this will throw an error
}); addOnLoad(function() {
console.log("Init: 2");
});
我们会发现这样的结果:
Init: 1 Error: DOES_NOT_EXIST is not defined Init: 2
很完美!这就是我们想要的结果。所有的回调函数都被执行,并且我们也得到了第一个回调函数执行出错的消息。
但是我肯定你会问IE怎么实现呢(我有很好的听觉,哈哈)?MSIE不支持标准的事件分派系统。它有自己的方法:fireEvent,但是也只有
对真实的事件(e.g. click)才有效果。
我会用代码来帮我解释:
var currentHandler; if (document.addEventListener) { // We've seen this code already } else if (document.attachEvent) { // MSIE document.documentElement.fakeEvents = 0; // an expando property document.documentElement.attachEvent("onpropertychange", function(event) {
if (event.propertyName == "fakeEvents") {
// execute the callback
currentHandler();
}
}); dispatchFakeEvent = function(handler) {
// fire the propertychange event
document.documentElement.fakeEvents++;
};
}
我们可以使用元素或对象的propertychange事件来帮助我们完成触发。
总结
我已经展示了如何用原生的事件系统来触发自定义事件。js库的作者们应该可以发现这种模型可以被扩展到跨浏览器的自定义实现上。
更新
有些人建议使用setTimeout。这是我的答复:
对于这个特殊的例子,定时器是可以正常工作的。这只是一个论证这种技术的简单例子而已。这种混合方法的真正好处在于其他的自定义事件。大多数的js库用回调模式实现自定义事件。就像我之前论证的,回调模式很脆弱。用定时器来进行事件分派在某种程度上是可以,但是它并不是真正的事件系统。在 实际的事件系统中,事件被依次分派。还有其他的问题,比如删除事件或者阻止事件冒泡,这无法用定时器实现。
这篇文章的重点是我提出了一种“将回调系统包裹在真正事件分派系统的自定义事件”实现。它会在IE下也真正触发自定义事件。如果你基于事件代理来创建
事件系统,那么这种技术可能会很有用。
个人见解:
如果仅仅是“实现多个回调函数互相独立执行”,那么可以使用一种方法,也正是原文中评论之一的做法:
try {
callback();
}
catch(e){
setTimeout(function(){
throw e;
}, 0);
}
这样可以实现回调之间独立执行,并且异步抛出执行错误。
但正如DE所说,他的目的不仅仅是解决上述问题,而是深入到更底层,颠覆自定义事件的固有实现模式--回调模式,采用基于伪事件的触发完成自定义事件的方法。而现在
很多库也借鉴了这种方法,确实也证实了DE的伟大之处。
以后还是应该多跟着巨人后面走走,拾人牙慧也不是不可以。
Callbacks vs Events的更多相关文章
- Python Twisted、Reactor
catalogue . Twisted理论基础 . 异步编程模式与Reactor . Twisted网络编程 . reactor进程管理编程 . Twisted并发连接 1. Twisted理论基础 ...
- 原生JS、CSS实现的转盘效果(目前仅支持webkit)
这是一个原生JS.CSS实现的转盘效果(题目在这:http://www.cnblogs.com/arfeizhang/p/turntable.html),花了半个小时左右,准备睡觉,所以先贴一段代码, ...
- DPDK中断机制简析
DPDK通过在线程中使用epoll模型,监听UIO设备的事件,来模拟操作系统的中断处理. 一.中断初始化 在rte_eal_intr_init()函数中初始化中断.具体如下: 1.首先初始化intr_ ...
- 探讨 : Host在IIS上的WCF Service的执行方式
一个WCF请求由两个线程来完成 运行在IIS上的WCF service, 你可能会注意到一个比较有趣的现象. 当WCF service接收到一个请求时, 这个请求实际上会有两个线程在执行这个请求. 一 ...
- Storm源码分析--Nimbus-data
nimbus-datastorm-core/backtype/storm/nimbus.clj (defn nimbus-data [conf inimbus] (let [forced-schedu ...
- Netty的核心组件
Netty的主要组成模块: Channels Callbacks Futures Events 和 handlers 这些模块代表了不同类型的概念:资源,逻辑和通知.你的应用将会利用这些模块来获取网络 ...
- [转] 理解CheckPoint及其在Tensorflow & Keras & Pytorch中的使用
作者用游戏的暂停与继续聊明白了checkpoint的作用,在三种主流框架中演示实际使用场景,手动点赞. 转自:https://blog.floydhub.com/checkpointing-tutor ...
- Netty实战 - 1. 基本概念
1. Netty简介 Netty是由JBOSS提供的一个java开源框架.它提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.Netty是一个基于NI ...
- vue弹窗组件
文件结构 component.vue <template> <div class="_vuedals" v-show="show"> & ...
随机推荐
- 向mysql中插入Date类型的数据
先看数据库表的定义 date字段为sql.date类型.我要向其中插入指定的日期和当前日期. 一.插入当前日期 思路:先获取当前系统,在将当前系统时间转换成sql类型的时间,然后插入数据库.代码如下 ...
- 【Python】引用计数
一.概述 要保持追踪内存中的对象,Python使用了引用计数这一简单的技术. 二.引用计数的增减 2.1 增加引用计数 当对象被创建并(将其引用)赋值给变量时,该对象的引用计数被设置为1. 对象的引用 ...
- 【hihoCoder】1288 : Font Size
题目:http://hihocoder.com/problemset/problem/1288 手机屏幕大小为 W(宽) * H(长),一篇文章有N段,每段有ai个字,要求使得该文章占用的页数不超过P ...
- Google云平台对于2014世界杯半决赛的预测,德国阿根廷胜!
由于本人是个足球迷,前段日子Google利用自己云平台预测世界杯八进四的比赛并取得了75%的正确率的事情让我振动不小.虽然这些年一直听说大数据的预测和看趋势能力如何如何强大,但这次的感受更加震撼,因为 ...
- 驱动编程:NtReadVirtualMemory
NtReadVirtualMemory函数位于ntdll中,作用就是把用户态的函数调用翻译成相应的系统调用,进入内核态.内核中一般有一个相同名字的处理函数,接收到该类型的系统调用后做实际的工作. NT ...
- 安卓中級教程(11):深入研究餓了麼的各個java檔運作關係(1)
package com.example.ele_me.activity; import android.annotation.SuppressLint; import android.app.Acti ...
- 学习笔记:delphi多线程知识
最近一直在温习旧的知识,刚好学习了一下Java的线程安全方面的知识,今天想起之前一直做的Delphi开发,所以还是有必要温习一下,看看这些不同的编程语言有什么不同之处. Delphi的线程同步方法: ...
- 迷你MVVM框架 avalonjs 入门教程
新官网 请不要无视这里,这里都是链接,可以点的 OniUI组件库 学习教程 视频教程: 地址1 地址2 关于AvalonJs 开始的例子 扫描 视图模型 数据模型 绑定 作用域绑定(ms-contro ...
- ASP.NET Web.config
分析: .NET Web 应用程序的配置信息(如最常用的设置ASP.Net Web 应用程序的身份验证方式),它可以出现在应用程序的每一个目录中.当你通过VB.NET新 建 一个Web应用程序后,默认 ...
- [erl] erlang 进程注册和注销
想要注册一个进程,必须先要创建一个进程. 如何创建一个进程,可以使用spawn.spawn_link,它们虽然都能创建进程,但是也有微妙的区别: 1)当前进程中创建一个并行进程,当被生成的进程崩溃时, ...