天天看点

探索Javascript异步编程

异步编程带来的问题在客户端javascript中并不明显,但随着服务器端javascript越来越广的被使用,大量的异步io操作使得该问题变得明显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。

探索Javascript异步编程

笔者在之前的一片博客中简单的讨论了python和javascript的异同,其实作为一种编程语言javascript的异步编程是一个非常值得讨论的有趣话题。

javascript 异步编程简介

回调函数和异步执行

所谓的异步指的是函数的调用并不直接返回执行的结果,而往往是通过回调函数异步的执行。

我们先看看回调函数是什么:

var fn = function(callback) { 

    // do something here 

    ... 

    callback.apply(this, para); 

}; 

var mycallback = function(parameter) { 

    // do someting in customer callback 

// call the fn with callback as parameter 

fn(mycallback); 

回调函数,其实就是调用用户提供的函数,该函数往往是以参数的形式提供的。回调函数并不一定是异步执行的。比如上述的例子中,回调函数是被同步执行的。大部分语言都支持回调,c++可用通过函数指针或者回调对象,java一般也是使用回调对象。

在javascript中有很多通过回调函数来执行的异步调用,例如settimeout()或者setinterval()。

settimeout(function(){ 

    console.log("this will be exectued after 1 second!"); 

},1000); 

以上的例子中,settimeout直接返回,匿名函数会在1000毫秒(不一定能保证是1000毫秒)后异步触发并执行,完成打印控制台的操作。也就是

说在异步操作的情境下,函数直接返回,把控制权交给回调函数,回调函数会在以后的某一个时间片被调度执行。那么为什么需要异步呢?为什么不能直接在当前函

数中完成操作呢?这就需要了解javascript的线程模型了。

javascript线程模型和事件驱动

javascript

最初是被设计成在浏览器中辅助提供html的交互功能。在浏览器中都包含一个javascript引擎,javscript程序就运行在这个引擎之中,并

且只有一个线程。单线程能都带来很多优点,程序员们可以很开心的不用去考虑诸如资源同步,死锁等多线程阻塞式编程所需要面对的恼人的问题。但是很多人会

问,既然javascript是单线程的,那它又如何能够异步的执行呢?

这 就需要了解到javascript在浏览器中的事件驱动(event driven)机制。事件驱动一般通过事件循环(event

loop)和事件队列(event

queue)来实现的。假定浏览器中有一个专门用于事件调度的实例(该实例可以是一个线程,我们可以称之为事件分发线程event dispatch

thread),该实例的工作就是一个不结束的循环,从事件队列中取出事件,处理所有很事件关联的回调函数(event

handler)。注意回调函数是在javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。

event loop code:

while(true) { 

 var event = eventqueue.pop(); 

 if(event && event.handler) { 

     event.handler.execute(); // execute the callback in javascript thread 

 } else { 

     sleep(); //sleep some time to release the cpu do other stuff 

 } 

通过事件驱动机制,我们可以想象javascript的编程模型就是响应一系列的事件,执行对应的回调函数。很多ui框架都采用这样的模型(例如java swing)。

那为什要异步呢,同步不是很好么?

异步的主要目的是处理非阻塞,在和html交互的过程中,会需要一些io操作(典型的就是ajax请求,脚本文件加载),如果这些操作是同步的,就会阻塞其它操作,用户的体验就是页面失去了响应。

综上所述javascript通过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的io操作。

javascript异步编程带来的挑战

javascript的单线程模型有很多好处,但同时也带来了很多挑战。

代码可读性

想象一下,如果某个操作需要经过多个非阻塞的io操作,每一个结果都是通过回调,程序有可能会看上去像这个样子。

operation1(function(err, result) { 

    operation2(function(err, result) { 

        operation3(function(err, result) { 

            operation4(function(err, result) { 

                operation5(function(err, result) { 

                    // do something useful 

                }) 

            }) 

        }) 

    }) 

}) 

我们称之为意大利面条式(spaghetti)的代码。这样的代码很难维护。这样的情况更多的会发生在server side的情况下。

流程控制

异步带来的另一个问题是流程控制,举个例子,我要访问三个网站的内容,当三个网站的内容都得到后,合并处理,然后发给后台。代码可以这样写:

var urls = ['url1','url2','url3']; 

var result = []; 

for (var i = 0, len = urls.length(); i < len; i++ ) { 

    $.ajax({ 

        url: urls[i], 

        context: document.body, 

        success: function(){ 

          //do something on success 

          result.push("one of the request done successfully"); 

          if (result.length === urls.length()) { 

              //do something when all the request is completed successfully 

          } 

        }}); 

上述代码通过检查result的长度的方式来决定是否所有的请求都处理完成,这是一个很丑陋方法,也很不可靠。

异常和错误处理

通过上一个例子,我们还可以看出,为了使程序更健壮,我们还需要加入异常处理。 在异步的方式下,异常处理分布在不同的回调函数中,我们无法在调用的时候通过try…catch的方式来处理异常, 所以很难做到有效,清楚。

更好的javascript异步编程方式

“这是最好的时代,也是最糟糕的时代”

为了解决javascript异步编程带来的问题,很多的开发者做出了不同程度的努力,提供了很多不同的解决方案。然而面对如此众多的方案应该如何选择呢?我们这就来看看都有哪些可供选择的方案吧。

promise

promise 对 象曾经以多种形式存在于很多语言中。这个词最先由c++工程师用在xanadu 项目中,xanadu 项目是web

应用项目的先驱。随后promise 被用在e编程语言中,这又激发了python 开发人员的灵感,将它实现成了twisted

框架的deferred 对象。

2007 年,promise 赶上了javascript 大潮,那时dojo

框架刚从twisted框架汲取灵感,新增了一个叫做dojo.deferred 的对象。也就在那个时候,相对成熟的dojo

框架与初出茅庐的jquery 框架激烈地争夺着人气和名望。2009 年,kris zyp 有感于dojo.deferred

的影响力提出了commonjs 之promises/a 规范。同年,node.js 首次亮相。

编程的概念中,future,promise,和delay表示同一个概念。promise翻译成中文是“承诺”,也就是说给你一个东西,我保证未来能够

做到,但现在什么都没有。它用来表示异步操作返回的一个对象,该对象是用来获取未来的执行结果的一个代理,初始值不确定。许多语言都有对promise的

支持。

promise的核心是它的then方法,我们可以使用这个方法从异步操作中得到返回值,或者是异常。then有两个可选参数(有的实现是三个),分别处理成功和失败的情景。

var promise = dosomethingaync() 

promise.then(onfulfilled, onrejected) 

步调用dosomethingaync返回一个promise对象promise,调用promise的then方法来处理成功和失败。这看上去似乎并没

有很大的改进。仍然需要回调。但是和以前的区别在于,首先异步操作有了返回值,虽然该值只是一个对未来的承诺;其次通过使用then,程序员可以有效的控

制流程异常处理,决定如何使用这个来自未来的值。

对于嵌套的异步操作,有了promise的支持,可以写成这样的链式操作:

operation1().then(function (result1) { 

    return operation2(result1) 

}).then(function (result2) { 

    return operation3(result2); 

}).then(function (result3) { 

    return operation4(result3); 

}).then(function (result4) { 

    return operation5(result4) 

}).then(function (result5) { 

    //and so on 

}); 

promise提供更便捷的流程控制,例如promise.all()可以解决需要并发的执行若干个异步操作,等所有操作完成后进行处理。

var p1 = async1(); 

var p2 = async2(); 

var p3 = async3(); 

promise.all([p1,p2,p3]).then(function(){ 

    // do something when all three asychronized operation finished 

对于异常处理,

doa() 

  .then(dob) 

  .then(null,function(error){ 

      // error handling here 

  }) 

如果doa失败,它的promise会被拒绝,处理链上的下一个onrejected会被调用,在这个例子中就是匿名函数function(error){}。比起原始的回调方式,不需要在每一步都对异常进行处理。这生了不少事。

<a target="_blank" href="http://documentup.com/kriskowal/q">q</a>

<a target="_blank" href="https://github.com/tildeio/rsvp.js">rsvp.js</a>

<a target="_blank" href="https://github.com/cujojs/when">when.js</a>

<a target="_blank" href="http://mochi.github.io/mochikit/doc/html/mochikit/async.html">mochikit.async</a>

<a target="_blank" href="https://github.com/futuresjs">futurejs</a>

<a target="_blank" href="https://github.com/kriszyp/node-promise">node-promise</a>

<a target="_blank" href="http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx">winjs</a>

如果你有选择困难综合症,面对这么多的开源库不知道如何决断,先不要急,这还只是一部分,还有一些库没有或者不完全采用promise的概念

non-promise

下面列出了其它的一些开源的库,也可以帮助解决javascript中异步编程所遇到的诸多问题,它们的解决方案各不相同,我这里就不一一介绍了。大家有兴趣可以去看看或者试用一下。

<a target="_blank" href="https://github.com/laverdet/node-fibers">node-fibers</a>

<a target="_blank" href="https://github.com/sage/streamlinejs">streamlinejs</a>

<a target="_blank" href="https://github.com/creationix/step">step</a>

<a target="_blank" href="https://github.com/willconant/flow-js">flow-js</a>

<a target="_blank" href="https://github.com/caolan/async">async</a>

<a target="_blank" href="https://github.com/fjakobs/async.js">async.js</a>

<a target="_blank" href="https://github.com/isaacs/slide-flow-control">slide-flow-control</a>

non-3rd party

其实,为了解决javascript异步编程带来的问题,不一定非要使用promise或者其它的开源库,这些库提供了很好的模式,但是你也可以通过有针对性的设计来解决。

比如,对于层层回调的模式,可以利用消息机制来改写,假定你的系统中已经实现了消息机制,你的code可以写成这样:

var co = require('co'); 

var fs = require('fs'); 

var stat = function(path) { 

  return function(cb){ 

    fs.stat(path,cb); 

  } 

}; 

var readfile = function(filename) { 

    fs.readfile(filename,cb); 

co(function *() { 

  var stat = yield stat('./readme.md'); 

  var content = yield readfile('./readme.md'); 

})(); 

这样我们就把嵌套的异步调用,改写成了顺序执行的事件处理。

下一代javscript对异步编程的增强

ecmascript6

nodejs的开发版v0.11已经可以支持es6的一些新的特性,使用node –harmony命令来运行对es6的支持。

co、thunk、koa

co是一个异步流程简化的工具,它利用generator把一层层嵌套的调用变成同步的写法。

通过co可以把异步的fs.readfile当成同步一样调用,只需要把异步函数fs.readfile用闭包的方式封装。

利用thunk可以进一步简化为如下的code, 这里thunk的作用就是用闭包封装异步函数,返回一个生成函数的函数,供生成器来调用。

利用co可以串行或者并行的执行异步调用。

var thunkify = require('thunkify'); 

var stat = thunkify(fs.stat); 

var readfile = thunkify(fs.readfile); 

串行

  var a = yield request(a); 

  var b = yield request(b); 

并行

 var res = yield [request(a), request(b)]; 

总结

步编程带来的问题在客户端javascript中并不明显,但随着服务器端javascript越来越广的被使用,大量的异步io操作使得该问题变得明

显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。

同时,随着es6的定义,javascript的语法变得越来越丰富,更多的功能带来了很多便利,然而原本简洁,单一目的的javascript变得复杂,也要承担更多的任务。javascript何去何从,让我们拭目以待。

来源:51cto