前言:本文翻译自Dean Edwards的一篇文章,原文地址:http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/

  文章主要指出了用“回调模式实现自定义事件”的一些弊端,同时提出了一种解决方案,即将回调的函数包装成原生事件,利用事件系统触发  

来完成回调的触发。


  大多数主流的js库都声称他们支持一种或多种形式的自定义事件。比如,jQuery,YUI以及Dojo他们都支持自定义事件“document ready”。然而

这些自定义事件的实现往往使用的是一种回调模式。

  回调系统(模式)往往需要一个数组来存储回调函数。如果当前的事件被处罚,则回调系统会轮询这个数组,并依次调用这些回调函数。听起来这样

实现是可以的,究竟会出现什么问题呢?在我回答之前,先看看例子。

  下面是一个简单的例子,使用了DOMContentLoaded事件来完成两个相互独立的初始化:

  

  1. document.addEventListener("DOMContentLoaded", function() {
  2. console.log("Init: 1");
  3. DOES_NOT_EXIST++; // this will throw an error
  4. }, false);
  5.  
  6. document.addEventListener("DOMContentLoaded", function() {
  7. console.log("Init: 2");
  8. }, false);

  你认为当事件出发时会出现什么效果呢?

  是这样的:

  

  1. Init: 1
  2.  
  3. Error: DOES_NOT_EXIST is not defined
  4.  
  5. Init: 2

  关键在于,这两个函数都执行了,第一个函数在执行时抛出错误,但并不影响第二个函数的执行。

问题所在

  现在我们尝试下用“回调模式”实现自定义事件的系统。在这里,使用jQuery库。

  

  1. $(document).ready(function() {
  2. console.log("Init: 1");
  3. DOES_NOT_EXIST++; // this will throw an error
  4. });
  5.  
  6. $(document).ready(function() {
  7. console.log("Init: 2");
  8. });

  我们会在console中看到如下结果:

  

  1. Init: 1
  2.  
  3. Error: DOES_NOT_EXIST is not defined

  

  问题很明显,用回调模式实现自定义事件是很脆弱的。如果任何一个回调函数抛出错误,那么随后的回调函数将不会被执行。实际上,这也意味着一个

写的很烂的插件有可能会阻止其他插件的初始化或正常工作。

  Dojo也和jQuery一样有着相同的问题。但是YUI则有些不同的实现。它在分派事件(事件执行)系统中用try/catch块将其包裹住。这样,即使其中一个

回调执行出错也会继续执行下一个回调函数,而且不会抛出错误:

  

  1. YAHOO.util.Event.onDOMReady(function() {
  2. console.log("Init: 1");
  3. DOES_NOT_EXIST++; // this will throw an error
  4. });
  5.  
  6. YAHOO.util.Event.onDOMReady(function() {
  7. console.log("Init: 2");
  8. });

  结果:

  

  1. Init: 1
  2.  
  3. Init: 2

  确实如此,你看不到第一个回调的错误。

  但是,我们需要寻找真正的解决(兼容)方案。

解决方案

  可以将回调模式和真实事件触发结合在一起混合使用。我们可以出发一个伪事件,并在该事件内,执行回调函数。每个回调函数都拥有其自己的执行上下文。如果在伪事件中出现错误(译者注:什么意思?当伪事件的回调函数出现错误?)也不会影响我们的回调系统。

  这听起来可能有些复杂,我会用例子来解释它:

  

  1. var currentHandler;
  2.  
  3. if (document.addEventListener) {
  4. document.addEventListener("fakeEvents", function() {
  5. // execute the callback
  6. currentHandler();
  7. }, false);
  8.  
  9. var dispatchFakeEvent = function() {
  10. var fakeEvent = document.createEvent("UIEvents");
  11. fakeEvent.initEvent("fakeEvents", false, false);
  12. document.dispatchEvent(fakeEvent);
  13. };
  14. } else { // MSIE
  15.  
  16. // I'll show this code later
  17. }
  18.  
  19. var onLoadHandlers = [];
  20. function addOnLoad(handler) {
  21. onLoadHandlers.push(handler);
  22. };
  23.  
  24. onload = function() {
  25. for (var i = 0; i < onLoadHandlers.length; i++) {
  26. currentHandler = onLoadHandlers[i];
  27. dispatchFakeEvent();
  28. }
  29. };
  30. //我们在这里绑定事件:
  31.  
  32. addOnLoad(function() {
  33. console.log("Init: 1");
  34. DOES_NOT_EXIST++; // this will throw an error
  35. });
  36.  
  37. addOnLoad(function() {
  38. console.log("Init: 2");
  39. });

  我们会发现这样的结果:

  

  1. Init: 1
  2.  
  3. Error: DOES_NOT_EXIST is not defined
  4.  
  5. Init: 2

  

  很完美!这就是我们想要的结果。所有的回调函数都被执行,并且我们也得到了第一个回调函数执行出错的消息。

  但是我肯定你会问IE怎么实现呢(我有很好的听觉,哈哈)?MSIE不支持标准的事件分派系统。它有自己的方法:fireEvent,但是也只有

对真实的事件(e.g. click)才有效果。

  我会用代码来帮我解释:

  

  1. var currentHandler;
  2.  
  3. if (document.addEventListener) {
  4.  
  5. // We've seen this code already
  6.  
  7. } else if (document.attachEvent) { // MSIE
  8.  
  9. document.documentElement.fakeEvents = 0; // an expando property
  10.  
  11. document.documentElement.attachEvent("onpropertychange", function(event) {
  12. if (event.propertyName == "fakeEvents") {
  13. // execute the callback
  14. currentHandler();
  15. }
  16. });
  17.  
  18. dispatchFakeEvent = function(handler) {
  19. // fire the propertychange event
  20. document.documentElement.fakeEvents++;
  21. };
  22. }

  我们可以使用元素或对象的propertychange事件来帮助我们完成触发。

总结

  我已经展示了如何用原生的事件系统来触发自定义事件。js库的作者们应该可以发现这种模型可以被扩展到跨浏览器的自定义实现上。

更新

  有些人建议使用setTimeout。这是我的答复:

  1. 对于这个特殊的例子,定时器是可以正常工作的。这只是一个论证这种技术的简单例子而已。这种混合方法的真正好处在于其他的自定义事件。大多数的js库用回调模式实现自定义事件。就像我之前论证的,回调模式很脆弱。用定时器来进行事件分派在某种程度上是可以,但是它并不是真正的事件系统。在 实际的事件系统中,事件被依次分派。还有其他的问题,比如删除事件或者阻止事件冒泡,这无法用定时器实现。

  这篇文章的重点是我提出了一种“将回调系统包裹在真正事件分派系统的自定义事件”实现。它会在IE下也真正触发自定义事件。如果你基于事件代理来创建

事件系统,那么这种技术可能会很有用。

  

个人见解:

  如果仅仅是“实现多个回调函数互相独立执行”,那么可以使用一种方法,也正是原文中评论之一的做法:

  

  1. try {
  2. callback();
  3. }
  4. catch(e){
  5. setTimeout(function(){
  6. throw e;
  7. }, 0);
  8. }

  这样可以实现回调之间独立执行,并且异步抛出执行错误。

  但正如DE所说,他的目的不仅仅是解决上述问题,而是深入到更底层,颠覆自定义事件的固有实现模式--回调模式,采用基于伪事件的触发完成自定义事件的方法。而现在

很多库也借鉴了这种方法,确实也证实了DE的伟大之处。

  以后还是应该多跟着巨人后面走走,拾人牙慧也不是不可以。

Callbacks vs Events的更多相关文章

  1. Python Twisted、Reactor

    catalogue . Twisted理论基础 . 异步编程模式与Reactor . Twisted网络编程 . reactor进程管理编程 . Twisted并发连接 1. Twisted理论基础 ...

  2. 原生JS、CSS实现的转盘效果(目前仅支持webkit)

    这是一个原生JS.CSS实现的转盘效果(题目在这:http://www.cnblogs.com/arfeizhang/p/turntable.html),花了半个小时左右,准备睡觉,所以先贴一段代码, ...

  3. DPDK中断机制简析

    DPDK通过在线程中使用epoll模型,监听UIO设备的事件,来模拟操作系统的中断处理. 一.中断初始化 在rte_eal_intr_init()函数中初始化中断.具体如下: 1.首先初始化intr_ ...

  4. 探讨 : Host在IIS上的WCF Service的执行方式

    一个WCF请求由两个线程来完成 运行在IIS上的WCF service, 你可能会注意到一个比较有趣的现象. 当WCF service接收到一个请求时, 这个请求实际上会有两个线程在执行这个请求. 一 ...

  5. Storm源码分析--Nimbus-data

    nimbus-datastorm-core/backtype/storm/nimbus.clj (defn nimbus-data [conf inimbus] (let [forced-schedu ...

  6. Netty的核心组件

    Netty的主要组成模块: Channels Callbacks Futures Events 和 handlers 这些模块代表了不同类型的概念:资源,逻辑和通知.你的应用将会利用这些模块来获取网络 ...

  7. [转] 理解CheckPoint及其在Tensorflow & Keras & Pytorch中的使用

    作者用游戏的暂停与继续聊明白了checkpoint的作用,在三种主流框架中演示实际使用场景,手动点赞. 转自:https://blog.floydhub.com/checkpointing-tutor ...

  8. Netty实战 - 1. 基本概念

    1. Netty简介 Netty是由JBOSS提供的一个java开源框架.它提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.Netty是一个基于NI ...

  9. vue弹窗组件

    文件结构 component.vue <template> <div class="_vuedals" v-show="show"> & ...

随机推荐

  1. 安卓端网页浏览过程中实时更新title的web实现

    $(function () { var scrollTop = 0, //缓存上一次触发scroll的时候的scrollTop值 appendIndex = 0, //由于第23行append这个操作 ...

  2. ES6的promise对象应该这样用

    ES6修补了一位Js修真者诸多的遗憾. 曾几何时,我这个小白从js非阻塞特性的坑中爬出来,当我经历了一些回调丑陋的写法和优化的尝试之后,我深深觉得js对于多线程阻塞式的开发语言而言,可能有着其太明显的 ...

  3. MySql怎样去掉某个字段最后的逗号或最后的字

    update 表 set 字段=left(字段,char_length(字段)-1) where right(字段,1)=',';

  4. ThinkPHP5 与 ThinkPHP3.* 之间的使用差异

    因为研究TP5时间不是很长,暂时先列以下几处差异: 1.过去的单字母函数已完全被替换掉,如下: S=>cache,C=>config,M/D=>model,U=>url,I=& ...

  5. jquery.ajax

    var params = {};//定义一个数组 var USERNAME= $("#USERNAME").val(); params["USERNAME"]= ...

  6. Android中SQLite数据库小计

    2016-03-16 Android数据库支持 本文节选并翻译<Enterprise Android - Programing Android Database Applications for ...

  7. 达梦7的试用 与SQLSERVER的简单技术对比

    达梦7的试用 与SQLSERVER的简单技术对比 达梦数据库公司推出了他们的数据库服务管理平台,可以在该平台使用达梦数据库而无须安装达梦7数据库 地址:http://online.dameng.com ...

  8. 三种观察者模式的C#实现

    系列主题:基于消息的软件架构模型演变 说起观察者模式,估计在园子里能搜出一堆来.所以写这篇博客的目的有两点: 观察者模式是写松耦合代码的必备模式,重要性不言而喻,抛开代码层面,许多组件都采用了Publ ...

  9. 在MotionBuilder中绑定C3D动作和模型

    [题外话] 实验室人手不足,虽然自己连MotionBuilder一点都没有用过,但是老板叫自己干也只能硬着头皮上了.本文详细介绍了MotionBuilder 2013中的摄像机操作以及在MotionB ...

  10. Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(3)

    Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(3) 到上一篇为止,拾取一个VBO里的单个图元的问题已经彻底解决了.那么来看下一个问题:一个场景里可能会有多个VBO,此时每个 ...