所谓"异步",简单说就是一个任务不是连续完成的,能够理解成该任务被人为分红两段,先执行第一段,而后转而执行其余任务,等作好了准备,再回过头执行第二段。node
好比,有一个任务是读取文件进行处理,任务的第一段是向操做系统发出请求,要求读取文件。而后,程序执行其余任务,等到操做系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫作异步。git
在JavaScript中处理异步传统方法是回调函数和Promise,如今多了一种方式:就是Generator函数。固然,Async函数是Generator的语法糖,也能够处理异步。github
传统的编程语言,早有异步编程的解决方案(实际上是多任务的解决方案)。其中有一种叫作"协程"(coroutine),意思是多个线程互相协做,完成异步任务。shell
协程有点像函数,又有点像线程。它的运行流程大体以下:编程
上面流程的协程A,就是异步任务,由于它分红两段(或多段)执行。json
举例来讲,读取文件的协程写法以下:api
function* asyncJob() { // ...其余代码 var f = yield readFile(fileA); // ...其余代码 }
上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其余协程。也就是说,yield命令是异步两个阶段的分界线。数组
协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续日后执行。它的最大优势,就是代码的写法很是像同步操做,若是去除yield命令,简直如出一辙。promise
Generator 函数是协程在ES6的实现,最大特色就是能够交出函数的执行权(即暂停执行)。并发
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操做须要暂停的地方,都用yield语句注明。
Generator 函数能够暂停执行和恢复执行,这是它能封装异步任务的根本缘由。除此以外,它还有两个特性,使它能够做为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
next返回值的value属性,是 Generator 函数向外输出数据;next方法还能够接受参数,向 Generator 函数体内输入数据。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
Generator 函数内部还能够部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了'); // 出错了
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
在 JavaScript语言中,Thunk函数替换的是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback); // Thunk版本的readFile(单参数版本) var Thunk = function (fileName) { return function (callback) { return fs.readFile(fileName, callback); }; }; var readFileThunk = Thunk(fileName); readFileThunk(callback);
上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。通过转换器处理,它变成了一个单参数函数,只接受回调函数做为参数。这个单参数版本,就叫作 Thunk 函数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的Thunk函数转换器:
// ES5版本 var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; }; // ES6版本 const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; };
使用上面的转换器,生成fs.readFile的 Thunk 函数:
var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);
生产环境的转换器,使用方式以下:
var thunkify = require('thunkify'); var fs = require('fs'); var read = thunkify(fs.readFile); read('package.json')(function(err, str){ // ... });
Thunkify 的源码与上面那个简单的转换器很是像:
function thunkify(fn) { return function() { var args = new Array(arguments.length); var ctx = this; for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } return function (done) { var called; args.push(function () { if (called) return;//多了一个检查机制,变量called确保回调函数只运行一次。 called = true; done.apply(null, arguments); }); try { fn.apply(ctx, args); } catch (err) { done(err); } } } };
Thunk 函数能够用于Generator函数的自动流程管理。
function* gen() { // ... } var g = gen(); var res = g.next(); while(!res.done){ console.log(res.value); res = g.next(); }
上面代码中,Generator函数gen会自动执行完全部步骤。
可是,这不适合异步操做。若是必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。这时,Thunk函数就能派上用处。以读取文件为例:
var fs = require('fs'); var thunkify = require('thunkify'); var readFileThunk = thunkify(fs.readFile); var gen = function* (){ var r1 = yield readFileThunk('/etc/fstab'); console.log(r1.toString()); var r2 = yield readFileThunk('/etc/shells'); console.log(r2.toString()); };
上面代码中,yield命令用于将程序的执行权移出 Generator 函数,那么就须要一种方法,将执行权再交还给 Generator 函数。
这种方法就是Thunk函数,它能够在回调函数里,将执行权交还给Generator函数。为了便于理解,咱们先看如何手动执行上面这个 Generator 函数:
var g = gen(); var r1 = g.next(); r1.value(function (err, data) { if (err) throw err; var r2 = g.next(data); r2.value(function (err, data) { if (err) throw err; g.next(data); }); });
仔细查看上面的代码,能够发现 Generator 函数的执行过程,实际上是将同一个回调函数,反复传入next方法的value属性。这使得咱们能够用递归来自动完成这个过程。
Thunk 函数真正的威力,在于能够自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器:
function run(fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); } next(); } function* g() { // ... } run(g);
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),而后判断 Generator 函数是否结束(result.done属性),若是没结束,就将next函数再传入 Thunk 函数(result.value属性),不然就直接退出。
有了这个执行器,执行 Generator 函数方便多了。无论内部有多少个异步操做,直接把 Generator 函数传入run函数便可。固然,前提是每个异步操做,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
var g = function* (){ var f1 = yield readFileThunk('fileA'); var f2 = yield readFileThunk('fileB'); // ... var fn = yield readFileThunk('fileN'); }; run(g);
上面代码中,函数g封装了n个异步的读取文件操做,只要执行run函数,这些操做就会自动完成。这样一来,异步操做不只能够写得像同步操做,并且一行代码就能够执行。
Thunk 函数并非Generator函数自动执行的惟一方案。由于自动执行的关键是,必须有一种机制,自动控制 Generator函数的流程,接收和交还程序的执行权。回调函数能够作到这一点,Promise 对象也能够作到这一点。
co 模块可让你不用编写Generator函数的执行器,它会自动执行Generator函数。
var co = require('co'); co(gen);
上面代码中,Generator函数只要传入co函数,就会自动执行,co函数返回一个Promise对象。
co(gen).then(function (){ console.log('Generator 函数执行完成'); });
上面代码中,等到Generator函数执行结束,就会输出一行提示。
为何 co 能够自动执行 Generator 函数?
前面说过,Generator 就是一个异步操做的容器。它的自动执行须要一种机制,当异步操做有告终果,可以自动交回执行权。
两种方法能够作到这一点:
co 模块其实就是将两种自动执行器(Thunk函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator函数的yield命令后面,只能是 Thunk函数或Promise对象。若是数组或对象的成员,所有都是 Promise 对象,也可使用 co,详见后文的例子。
沿用上面的例子,把fs模块的readFile方法包装成一个 Promise 对象:
var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) return reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
而后,手动执行上面的 Generator 函数:
var g = gen(); g.next().value.then(function(data){ g.next(data).value.then(function(data){ g.next(data); }); });
手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就能够写出一个自动执行器:
function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next(); } run(gen);
co 就是上面那个自动执行器的扩展,它的源码只有几十行,很是简单。
首先,co 函数接受 Generator 函数做为参数,返回一个 Promise 对象。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { }); }
在返回的 Promise 对象里面,co 先检查参数gen是否为 Generator 函数。若是是,就执行该函数,获得一个内部指针对象;若是不是就返回,并将 Promise 对象的状态改成resolved。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); }); }
接着,co 将 Generator 函数的内部指针对象的next方法,包装成onFulfilled函数。这主要是为了可以捕捉抛出的错误。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } }); }
最后,就是关键的next函数,它会反复调用自身。
function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected( new TypeError( 'You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"' ) ); }
上面代码中,next函数的内部代码,一共只有四行命令。
第一行,检查当前是否为 Generator 函数的最后一步,若是是就返回。
第二行,确保每一步的返回值,是 Promise 对象。
第三行,使用then方法,为返回值加上回调函数,而后经过onFulfilled函数再次调用next函数。
第四行,在参数不符合要求的状况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改成rejected,从而终止执行。
co 支持并发的异步操做,即容许某些操做同时进行,等到它们所有完成,才进行下一步。
这时,要把并发的操做都放在数组或对象里面,跟在yield语句后面。
// 数组的写法 co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2) ]; console.log(res); }).catch(onerror); // 对象的写法 co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); }).catch(onerror);
下面是另外一个例子。
co(function* () { var values = [n1, n2, n3]; yield values.map(somethingAsync); }); function* somethingAsync(x) { // do something async return y }
上面的代码容许并发三个somethingAsync异步操做,等到它们所有完成,才会进行下一步。