笔者在以前的一片博客中简单的讨论了Python和Javascript的异同,其实做为一种编程语言Javascript的异步编程是一个很是值得讨论的有趣话题。 javascript
所谓的异步指的是函数的调用并不直接返回执行的结果,而每每是经过回调函数异步的执行。 html
咱们先看看回调函数是什么: java
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通常也是使用回调对象。 node
在Javascript中有不少经过回调函数来执行的异步调用,例如setTimeout()或者setInterval()。
jquery
setTimeout(function(){ console.log("this will be exectued after 1 second!"); },1000);
在以上的例子中,setTimeout直接返回,匿名函数会在1000毫秒(不必定能保证是1000毫秒)后异步触发并执行,完成打印控制台的操做。也就是说在异步操做的情境下,函数直接返回,把控制权交给回调函数,回调函数会在之后的某一个时间片被调度执行。那么为何须要异步呢?为何不能直接在当前函数中完成操做呢?这就须要了解Javascript的线程模型了。 git
Javascript最初是被设计成在浏览器中辅助提供HTML的交互功能。在浏览器中都包含一个Javascript引擎,Javscript程序就运行在这个引擎之中,而且只有一个线程。单线程能都带来不少优势,程序员们能够很开心的不用去考虑诸如资源同步,死锁等多线程阻塞式编程所须要面对的恼人的问题。可是不少人会问,既然Javascript是单线程的,那它又如何可以异步的执行呢?
程序员
这就须要了解到Javascript在浏览器中的事件驱动(event driven)机制。事件驱动通常经过事件循环(event loop)和事件队列(event queue)来实现的。假定浏览器中有一个专门用于事件调度的实例(该实例能够是一个线程,咱们能够称之为事件分发线程event dispatch thread),该实例的工做就是一个不结束的循环,从事件队列中取出事件,处理全部很事件关联的回调函数(event handler)。注意回调函数是在Javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。 es6
Event Loop Code: github
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)。 ajax
那为什要异步呢,同步不是很好么?
异步的主要目的是处理非阻塞,在和HTML交互的过程当中,会须要一些IO操做(典型的就是Ajax请求,脚本文件加载),若是这些操做是同步的,就会阻塞其它操做,用户的体验就是页面失去了响应。
综上所述Javascript经过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的IO操做。
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异步编程带来的问题,不少的开发者作出了不一样程度的努力,提供了不少不一样的解决方案。然而面对如此众多的方案应该如何选择呢?咱们这就来看看都有哪些可供选择的方案吧。
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){}。比起原始的回调方式,不须要在每一步都对异常进行处理。这生了很多事。
以上只是对于Promise概念的简单陈述,Promise拥有许多不一样规范建议(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和开源实现。你们能够参考一下的这些连接。
若是你有选择困难综合症,面对这么多的开源库不知道如何决断,先不要急,这还只是一部分,还有一些库没有或者不彻底采用Promise的概念
下面列出了其它的一些开源的库,也能够帮助解决Javascript中异步编程所遇到的诸多问题,它们的解决方案各不相同,我这里就不一一介绍了。你们有兴趣能够去看看或者试用一下。
其实,为了解决Javascript异步编程带来的问题,不必定非要使用Promise或者其它的开源库,这些库提供了很好的模式,可是你也能够经过有针对性的设计来解决。
好比,对于层层回调的模式,能够利用消息机制来改写,假定你的系统中已经实现了消息机制,你的code能够写成这样:
eventbus.on("init", function(){ operationA(function(err,result){ eventbus.dispatch("ACompleted"); }); }); eventbus.on("ACompleted", function(){ operationB(function(err,result){ eventbus.dispatch("BCompleted"); }); }); eventbus.on("BCompleted", function(){ operationC(function(err,result){ eventbus.dispatch("CCompleted"); }); }); eventbus.on("CCompleted", function(){ // do something when all operation completed });
这样咱们就把嵌套的异步调用,改写成了顺序执行的事件处理。
更多的方式,请你们参考这篇文章,它提出了解决异步的五种模式:回调、观察者模式(事件)、消息、Promise和有限状态机(FSM)。
下一代的Javascript标准Harmony,也就是ECMAScript6正在酝酿中,它提出了许多新的语言特性,好比箭头函数、类(Class)、生成器(Generator)、Promise等等。其中Generator和Promise均可以被用于对异步调用的加强。
Nodejs的开发版V0.11已经能够支持ES6的一些新的特性,使用node --harmony命令来运行对ES6的支持。
koa是由Express原班人马(主要是TJ)打造,但愿提供一个更精简健壮的nodejs框架。koa依赖ES6中的Generator等新特性,因此必须运行在相应的Nodejs版本上。
利用Generator、co、Thunk,能够在Koa中有效的解决Javascript异步调用的各类问题。
co是一个异步流程简化的工具,它利用Generator把一层层嵌套的调用变成同步的写法。
var co = require('co'); var fs = require('fs'); var stat = function(path) { return function(cb){ fs.stat(path,cb); } }; var readFile = function(filename) { return function(cb){ fs.readFile(filename,cb); } }; co(function *() { var stat = yield stat('./README.md'); var content = yield readFile('./README.md'); })();
经过co能够把异步的fs.readFile当成同步同样调用,只须要把异步函数fs.readFile用闭包的方式封装。
利用Thunk能够进一步简化为以下的code, 这里Thunk的做用就是用闭包封装异步函数,返回一个生成函数的函数,供生成器来调用。
var thunkify = require('thunkify'); var co = require('co'); var fs = require('fs'); var stat = thunkify(fs.stat); var readFile = thunkify(fs.readFile); co(function *() { var stat = yield stat('./README.md'); var content = yield readFile('./README.md'); })();
利用co能够串行或者并行的执行异步调用。
串行
co(function *() { var a = yield request(a); var b = yield request(b); })();
并行
co(function *() { var res = yield [request(a), request(b)]; })();
异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript愈来愈广的被使用,大量的异步IO操做使得该问题变得明显。许多不一样的方法均可以解决这个问题,本文讨论了一些方法,但并不深刻。你们须要根据本身的状况选择一个适于本身的方法。
同时,随着ES6的定义,Javascript的语法变得愈来愈丰富,更多的功能带来了不少便利,然而本来简洁,单一目的的Javascript变得复杂,也要承担更多的任务。Javascript何去何从,让咱们拭目以待。