http://sporto.github.io/blog/2012/12/09/callbacks-listeners-promises/

http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html

 

当用 Javascript 处理异步(asynchronous )时,你可以使用很多工具。本文说明四个异步的方法和工具,以及它们的优势:回调(Callbacks)、监听(Listeners)、流程控制库(Control Flow Libraries)和 Promises。

示例场景


为了说明这四个工具,让我们创建一个简单的示例场景。

我们想查找(find)一些记录,然后处理(process)它们,最后返回处理后的结果。这两个操作(查找和处理)是异步的。

回调


回调是处理异步编程的最基本最公认的方式。

回调方式像如下形式:

finder([1, 2], function(results) {

    // do something 

});

在回调方式中,我们调用一个执行异步操作的函数。传递的其中一个参数是一个函数,当操作完成时,它会被调用。

设置

为了说明它们如何运行,我们需要两个函数,find 和 process,分别用来查找和处理记录。在实际中,这些函数会发出 AJAX 请求,并返回结果,但是现在,我们可以简单使用 timeout。

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(records);

    }, 1000);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(records);

    }, 1000);

}

使用回调

完成这些功能的代码像如下形式:

finder([1, 2], function (records) {

    processor(records, function(records) {

      console.log(records);

    });

});

finder 函数里有一个回调,processor 函数里也有一个回调。

通过传递另一个函数的引用,上面嵌套的回调可以写得更清晰。

function onProcessorDone(records){

  console.log(records);

}

 

function onFinderDone(records) {

    processor(records, onProcessorDone);

}

 

finder([1, 2], onFinderDone);

控制台将输出 [1,2,3,4,5,6]

完整示例如下所示:

// setup

 

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(records);

    }, 500);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(records);

    }, 500);

}

 

// using the callbacks

finder([1, 2], function (records) {

    processor(records, function(records) {

             console.log(records);       

    });

});

 

// or

 

function onProcessorDone(records){

    alert(records);   

}

 

function onFinderDone(records) {

    processor(records, onProcessorDone);

}

 

finder([1, 2], onFinderDone);

说明

  • 这是最常见的方式,比较熟悉,很容理解。
  • 在你的库/函数中很容易实现。

结论

如上所示,嵌套的回调将可能形成一个“泛滥”的局面,当你有很多嵌套等级时,很难阅读。但通过分割函数,如上所示,也很容易修复。

对于一个给定的事件,你只能传递一个回调,在很多情况下,这可能是一个很大的限制。

 

监听


监听也是很常见的方式,jQuery 和 其他 DOM 库都使用该方式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

监听方式像如下形式所示:

finder.on('done', function (event, records) {

  // do something

});

我们在一个对象上调用一个函数,该对象是一个添加的侦听器对象。在该函数中,我们传递想监听的事件名称和回调函数。 “on”是该功能的许多常见名称之一,其他常见名称如”bind”、“listen”、“addEventListener”、“observe”功能类似。

设置

现在为该示例做些设置,设置比上面的回调示例多些。

首先,我们需要两个对象,用于查询和处理记录。

var finder = {

    run: function (records) {

        var self = this;

        setTimeout(function () {

            records.push(3, 4);

           self.trigger('done', [records]);

        }, 1000);

    }

}

var processor = {

    run: function (records) {

        var self = this;

        setTimeout(function () {

            records.push(5, 6);

            self.trigger('done', [records]);

        }, 1000);

    }

 }

注意,当运行完成时,它们会调用一个触发器(trigger )的方法,我们通过 min-in 向这些对象添加方法。另外,“trigger ”也是该功能的常见名称之一,其他常见名称如“fire”、“publish”。

我们需要一个已经具备监听行为的 min-in 对象,在这种情况下,我们将依靠 jQuery:

var eventable = {

    on: function(event, cb) {

        $(this).on(event, cb);

    },

    trigger: function (event, args) {

        $(this).trigger(event, args);

    }

}

然后,把这个行为应用到我们的 finder 和 processor 对象:

$.extend(finder, eventable);

$.extend(processor, eventable);

现在,我们的对象就采用了监听,并触发事件。

使用监听

完成监听的代码,如下所示:

finder.on('done', function (event, records) {

  processor.run(records);

});

processor.on('done', function (event, records) {

    console.log(records);

});

finder.run([1,2]);

控制台将输出 [1,2,3,4,5,6]

完整代码如下所示:

// using listeners

var eventable = {

    on: function(event, cb) {

        $(this).on(event, cb);

    },

    trigger: function (event, args) {

        $(this).trigger(event, args);

    }

}

    

var finder = {

    run: function (records) {

            var self = this;

        setTimeout(function () {

            records.push(3, 4);

           self.trigger('done', [records]);            

        }, 500);

    }

}

var processor = {

    run: function (records) {

         var self = this;

        setTimeout(function () {

            records.push(5, 6);

            self.trigger('done', [records]);            

        }, 500);

    }

 }

 $.extend(finder, eventable);

 $.extend(processor, eventable);

    

finder.on('done', function (event, records) {

          processor.run(records);  

    });

processor.on('done', function (event, records) {

    alert(records);

});

finder.run([1,2]);

说明

  • 这是另一个很容易理解的方式。
  • 最大的优势在于,对每个对象的一个监听函数,没有限制,你可以向一个对象添加很多监听函数,如下所示:
finder

  .on('done', function (event, records) {

      // do something

  })

  .on('done', function (event, records) {

      // do something else

  });

结论

  • 该方式比回调麻烦点,因此,你可能会使用现成的库,如 jQuery, bean.js
  • 每个事件可以指定多个回调函数,有利于实现模块化。
  • 问题是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

 

流程控制库


流程控制库也是解决异步代码的很好方式。我特别喜欢的一个是 Async.js

使用 Async.js 的像代码如下所示:

async.series([

    function(){ ... },

    function(){ ... }

]);

设置(示例一)

我们也需要两个函数,跟其他例子一样,实际中,我们可能需要发送 AJAX 请求,并返回结果。现在,我们仅仅使用 timeout.

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(null, records);

    }, 1000);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(null, records);

    }, 1000);

}

节点持续传递的风格(The Node Continuation Passing Style)

注意,在上面函数内部,回调中使用的风格。

cb(null, records);

回调中第一个参数,若没有错误发生,则为空;否则为错误。这是在 Node.js 中常见的方式,Async.js 也使用这种方式。通过使用这种风格,Async.js 和回调之间的流程变得相当简单。

使用 Async

实现该功能的代码像如下形式所示:

async.waterfall([

    function(cb){

        finder([1, 2], cb);

    },

    processor,

    function(records, cb) {

        alert(records);

    }

]);

Async.js 关心,当之前的已经完成后,按顺序调用每个函数。注意,我们仅仅传递了“processor”函数,这是因为我们使用节点持续风格。正如你所看到的,代码很少,而且很容易理解。

完整代码如下所示:

// setup

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(null, records);

    }, 500);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(null, records);

    }, 500);

}

 

async.waterfall([

    function(cb){

        finder([1, 2], cb);

    },

    processor

    ,

    function(records, cb) {

        alert(records);

    }

]);

另一个设置(示例二)

现在,当做前段开发(front-end development)时,有一个遵照 callback(null, results) 声明的库,是不可能的。因此,一个更实际的示例如下所示:

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(records);

    }, 500);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(records);

    }, 500);

}

 

// using the finder and the processor

async.waterfall([

    function(cb){

        finder([1, 2], function(records) {

            cb(null, records)

        });

    },

    function(records, cb){

        processor(records, function(records) {

            cb(null, records);

        });

    },

    function(records, cb) {

        alert(records);

    }

]);

尽管这变得有点令人费解,但是至少你能够看到从上到下运行的流程。

完整代码如下所示:

// setup

 

function finder(records, cb) {

    setTimeout(function () {

        records.push(3, 4);

        cb(records);

    }, 500);

}

function processor(records, cb) {

    setTimeout(function () {

        records.push(5, 6);

        cb(records);

    }, 500);

}

 

async.waterfall([

    function(cb){

        finder([1, 2], function(records) {

            cb(null, records)

        });

    },

    function(records, cb){

        processor(records, function(records) {

            cb(null, records);

        });

    },

    function(records, cb) {

        alert(records);

    }

]);

说明

  • 一般,使用控制流程库得代码较容易理解,因为它遵循一个自然的顺序(从上到下)。这对回调和监听不是。

结论

  • 如果函数的声明不匹配,像第二个示例,那么你可以辩称,流量控制库在可读性方面提供的较少。

 

Promises


最后是我们的最终目的地。Promises 是非常强大的工具,是 CommonJS 工作组提出的一种规范,目的是为异步编程提供统一接口

使用 promises 代码像如下方式:

finder([1,2])

    .then(function(records) {

      // do something

    });

这将很大程度上取决于你使用的 promises 库,本例我使用 when.js。每一个异步任务返回一个 Promise 对象,该对象有一个 then 方法,允许指定回调函数。

设置

finder 和 processor 函数像如下所示:

function finder(records){

    var deferred = when.defer();

    setTimeout(function () {

        records.push(3, 4);

        deferred.resolve(records);

    }, 500);

    return deferred.promise;

}

function processor(records) {

     var deferred = when.defer();

    setTimeout(function () {

        records.push(5, 6);

        deferred.resolve(records);

    }, 500);

    return deferred.promise;

}

每个函数创建一个 deferred 对象,并返回一个 promise。然后,当结果到达时,它解析 deferred。

使用 promises

实现该功能的代码像如下所示:

finder([1,2])

    .then(processor)

    .then(function(records) {

            alert(records);

    });

该方法很简单,很容易理解。promises 使你代码很简洁,就像按照一个自然的流程。注意,在第一个回调,我们如何简单传递“processor”函数,这是因为这个函数返回一个 promise  自身,这样,所有一切都将很好地“流动”。

完整代码如下所示:

// using promises

function finder(records){

    var deferred = when.defer();

    setTimeout(function () {

        records.push(3, 4);

        deferred.resolve(records);

    }, 500);

    return deferred.promise;

}

function processor(records) {

     var deferred = when.defer();

    setTimeout(function () {

        records.push(5, 6);

        deferred.resolve(records);

    }, 500);

    return deferred.promise;

}

 

finder([1,2])

    .then(processor)

    .then(function(records) {

            alert(records);

    });

There is a lot to promises:

  • 作为常规对象来传递
  • 聚合到一个更大的 promises
  • 为失败的 promises 添加处理函数
  • 回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。

promises 的最大优势

现在,如果你认为,这就是 promises 所有,那么你正错过我认为的最大优势。Promises 具有一个回调、监听或是控制流程都没有的“绝招”。你可以向 promise 添加一个监听器,即使它已经被解析,在这种情况下,监听将会立即触发,这意味着,你不用担心,当你添加监听,事件是否已经发生。此功能同样适用于聚集的 promises。如下所示:

function log(msg) {

    document.write(msg + '<br />');

}

 

// using promises

function finder(records){

    var deferred = when.defer();

    setTimeout(function () {

        records.push(3, 4);

        log('records found - resolving promise');

        deferred.resolve(records);

    }, 100);

    return deferred.promise;

}

 

var promise = finder([1,2]);

 

// wait 

setTimeout(function () {

    // when this is called the finder promise has already been resolved

    promise.then(function (records) {

        log('records received');        

    });

}, 1500);

对在浏览器中处理用户交互来说,这是个巨大的功能。在复杂的应用程序中,你可能不是用户将采取的行动的现在顺序,因此,你可以使用 promises 来跟踪用户交互。如果有兴趣的话,参看 post

说明

  • Really powerful, you can aggregate promises, pass them around, or add listeners when already resolved.

结论

  • The least understood of all these tools.
  • They can get difficult to track when you have lots of aggregated promises with added listeners along the way.

 

结论


在我看来,以上解决异步变成的四种主要工具。希望对你理解它们有所帮助。

异步 JS: Callbacks, Listeners, Control Flow Libs 和 Promises【转载+翻译+整理】的更多相关文章

  1. Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises

    非常好的文章,讲javascript 的异步编程的. ------------------------------------------------------------------------- ...

  2. 你所必须掌握的三种异步编程方法callbacks,listeners,promise

    目录: 前言 Callbacks Listeners Promise 前言 coder都知道,javascript语言运行环境是单线程的,这意味着任何两行代码都不能同时运行.多任务同时进行时,实质上形 ...

  3. SSIS的 Data Flow 和 Control Flow

    Control Flow 和 Data Flow,是SSIS Design中主要用到的两个Tab,理解这两个Tab的作用,对设计更高效的package十分重要. 一,Control Flow 在Con ...

  4. [翻译] TensorFlow 分布式之论文篇 "Implementation of Control Flow in TensorFlow"

    [翻译] TensorFlow 分布式之论文篇 "Implementation of Control Flow in TensorFlow" 目录 [翻译] TensorFlow ...

  5. Control Flow in Tensorflow TF中的控制流解析

    写在前面 本文翻译自Tensorflow团队的文章Tensorflow Control Flow Implementation,部分内容加入了笔者自己的理解,如有不妥之处还望各位指教. 目录 概览 控 ...

  6. Control Flow 如何处理 Error

    在Package的执行过程中,如果在Data Flow中出现Error,那么Data Flow component能够将错误行输出,只需要在组件的ErrorOutput中进行简单地配置,参考<D ...

  7. 关于Control flow

    1.一个package包含一个control flow并且一个或多个data flow. (这个项目叫做 Integration services project,提供了三种不同类型的control  ...

  8. Core Java Volume I — 3.8. Control Flow

    3.8. Control FlowJava, like any programming language, supports both conditional statements and loops ...

  9. 异步JS:$.Deferred的使用

    异步JS:$.Deferred的使用 原文链接:http://www.html5rocks.com/en/tutorials/async/deferred/ 当我们构建一个平稳的,响应式的HTML5应 ...

随机推荐

  1. 基于设备树的TQ2440 DMA学习(1)—— 芯片手册

    作者 彭东林pengdonglin137@163.com 平台 TQ2440内核Linux4.9 概述 一直想抽时间学习一下DMA驱动,今天就以S3C2440为例,这款芯片的DMA控制器足够简单,也比 ...

  2. 测试RemObjects Pascal Script

    unit Unit1; interface usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, ...

  3. Java Dictionary 类存储键值

    字典(Dictionary) 字典(Dictionary) 类是一个抽象类,它定义了键映射到值的数据结构. 当你想要通过特定的键而不是整数索引来访问数据的时候,这时候应该使用Dictionary. 当 ...

  4. CentOS RabbitMQ 高可用(Mirrored)

    原文:https://www.sunjianhua.cn/archives/centos-rabbitmq.html 一.RabbitMQ 单节点 1.1.Windows 版安装配置 1.1.1 安装 ...

  5. Ant build.xml

    Ant的概念可能有些读者并不连接什么是Ant以及入可使用它,但只要使用通过Linux系统得读者,应该知道make这个命令.当编译Linux内核及一些软件的源程序时,经常要用这个命令.Make命令其实就 ...

  6. 【Devops】【docker】【CI/CD】关于jenkins构建成功后一步,执行的shell命令详解+jenkins容器运行宿主机shell命令的实现方法

    1.展示这段shell命令 +详解 #================================================================================= ...

  7. Android: INSTALL_FAILED_UPDATE_INCOMPATIBLE

    from://http://xusaomaiss.iteye.com/blog/393296 在反复安装android apk的时候,有的时候可能会遇到adb install错误,内容是:Failur ...

  8. 移动环境下DNS解析失败后的优化方案

    我们手机游戏中,通过上报收集到的数据来分析,发现相当多的一部分用户,在请求一些配置时会遇到无法解析的情况,或者域名的解析直接被拦截了. 特别是游戏的补丁包文件(放在CDN上),遇到的域名解析失败是最多 ...

  9. PHPthinking赠书了!

    [站长赠书]2014年10月第一期幸运用户  大家好,我是PHP开发学习门户的站长,小站建站一个多月,感谢大家以来的关注和支持,假设大家对本站有什么建议或者投稿,欢迎留言或者给我发邮件. 本站宣布对于 ...

  10. DNS使用的是TCP协议还是UDP协议(转)

    原文链接:DNS使用的是TCP协议还是UDP协议 DNS同时占用UDP和TCP端口53是公认的,这种单个应用协议同时使用两种传输协议的情况在TCP/IP栈也算是个另类.但很少有人知道DNS分别在什么情 ...