• 异步 JS: Callbacks, Listeners, Control Flow Libs 和 Promises【转载+翻译+整理】


    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.

     

    结论


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

  • 相关阅读:
    Jedis scan及其count的值
    redis中KEYS、SMEMBERS、SCAN 、SSCAN 的区别
    Windows环境下RabbitMQ的启动和停止命令
    HTTP状态码->HTTP Status Code
    给所有的input trim去空格
    git clone 使用用户名和密码
    ABA问题
    FIFO、LRU、LFU的含义和原理
    【phpstorm】破解安装
    【windows7】解决IIS 80端口占用问题(亲测)
  • 原文地址:https://www.cnblogs.com/liuning8023/p/3220492.html
Copyright © 2020-2023  润新知