大部分复制整理于以下四篇文章,侵删致歉。
Generator函数的含义与用法 Thunk函数的含义与用法 co函数库的含义与用法 async函数的含义与用法
协程coroutine 多个线程互相协作,完成异步任务。
协程A开始执行。
协程A执行到一半,进入暂停,执行权转移到协程B。
(一段时间后)协程B交还执行权。
协程A恢复执行。
1 2 3 4 5 function asnycJob ( ) { var f = yield readFile(fileA); }
上面代码的函数 asyncJob
是一个协程。yield
命令表示执行到此处,执行权将交给其他协程。也就是说,yield
命令是异步两个阶段的分界线。
协程遇到 yield
命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
Generator函数 Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
1 2 3 4 5 6 7 8 9 10 function * gen (x ) { var y = yield x + 2 ; return y; } ----- var g = gen(1 );g.next() g.next()
Generator 函数不同于普通函数,是可以暂停执行的,所以函数名之前要加星号 ,以示区别。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield
语句注明。
调用 Generator 函数,会返回一个内部指针(即遍历器 )g
。
调用指针 g
的 next
方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield
语句,上例是执行到 x + 2
为止。
换言之,next
方法的作用是分阶段执行 Generator 函数。每次调用 next
方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性
)。
value
属性是 yield
语句后面表达式的值 ,表示当前阶段的值;done
属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
数据交换 1 2 3 4 5 6 7 8 function * gen (x ) { var y = yield x + 2 ; return y; } var g = gen(1 );g.next() g.next(2 )
第一个 next
方法的 value 属性,返回表达式 x + 2
的值(3)
。
第二个 next
方法带有参数2
,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果(即yield
语句后面表达式的值),被函数体内的变量 y
接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)
。
错误处理 捕获函数体外错误
1 2 3 4 5 6 7 8 9 10 11 12 13 function * gen (x ) { try { var y = yield x + 2 ; } catch (e){ console .log(e); } return y; } var g = gen(1 );g.next(); g.throw('出错了' );
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw
方法抛出的错误,可以被函数体内的 try ... catch
代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
实例 Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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); });
Thunk Thunk 函数的含义和用法
1. 传值策略 1 2 3 4 5 6 7 8 var x = 1 ;function f (m ) { return m * 2 ; } f(x + 5 )
“传值调用”(call by value): f(x + 5)即f(6)
“传名调用”(call by name): f(x + 5)即(x + 5) * 2
2. Thunk函数 Thunk 函数是传名调用
的一种实现策略,用来替换某个表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function f (m ) { return m * 2 ; } f(x + 5 ); var thunk = function ( ) { return x + 5 ; }; function f (thunk ) { return thunk() * 2 ; }
3. Thunk 函数(JavaScript) 在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本 ,且只接受回调函数作为参数 。
1 2 3 4 5 6 7 8 9 10 11 12 fs.readFile(fileName, callback); var readFileThunk = Thunk(fileName);readFileThunk(callback); var Thunk = function (fileName ) { return function (callback ) { return fs.readFile(fileName, callback); }; };
Thunk 函数转换器 1 2 3 4 5 6 7 8 9 var Thunk = function (fn ) { return function ( ) { var args = Array .prototype.slice.call(arguments ); return function (callback ) { args.push(callback); return fn.apply(this , args); } }; };
1 2 3 var readFileThunk = Thunk(fs.readFile);readFileThunk(fileA)(callback);
4. Array.prototype.slice.call(arguments) arrayObj.slice(start, [end])
: 截取数组的一部分call([thisObj[,arg1[arg2[[argN]]]]])
: thisObj是一个对象的方法 arrg1~argN是参数
所以该方法用来把调用方法的参数截取出来 。
1 2 3 4 5 6 7 function test (a,b,c,d ) { var arg = Array .prototype.slice.call(arguments ,1 ); alert(arg); } test("a" ,"b" ,"c" ,"d" );
因为arguments并不是真正的数组对象而是Object,只是与数组类似而已,所以它并没有slice
这个方法,而Array.prototype.slice.call(arguments, 1)
可以理解成是让arguments转换成一个数组对象,让arguments具有slice()
方法。要是直接写arguments.slice(1)
会报错。
原理 `Array.prototype.slice.call(arguments)能将具有length属性的对象转成数组,除了IE下的节点集合(因为ie下的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var a={length :2 ,0 :'first' ,1 :'second' };console .log(Array .prototype.slice.call(a,0 ));var a={length :2 ,0 :'first' ,1 :'second' };console .log(Array .prototype.slice.call(a,1 ));var a={0 :'first' ,1 :'second' };console .log(Array .prototype.slice.call(a,0 ));function test ( ) { console .log(Array .prototype.slice.call(arguments ,0 )); console .log(Array .prototype.slice.call(arguments ,1 )); } test("a" ,"b" ,"c" );
将函数的实际参数转换成数组的方法
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments, 0);
1 2 3 4 var args = [];for (var i = 1 ; i < arguments .length; i++) { args.push(arguments [i]); }
5. Thunkify https://github.com/tj/node-thunkify
Turn a regular node function into one which returns a thunk, useful for generator-based flow control such as co.
1 2 3 4 5 6 7 8 var thunkify = require ('thunkify' );var fs = require ('fs' );var read = thunkify(fs.readFile);read('package.json' , 'utf8' )(function (err, str ) { });
6. 基于 Thunk 函数的 Generator 执行器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function run (fn ) { var gen = fn(); function next (err, data ) { var result = gen.next(data); if (result.done) return ; result.value(next); } next(); } var gen = function * ( ) { var f1 = yield readFile('fileA' ); var f2 = yield readFile('fileB' ); var fn = yield readFile('fileN' ); }; run(gen);
上面代码的 run
函数,就是一个 Generator 函数的自动执行器。 内部的 next
函数就是 Thunk 的回调函数。next
函数先将指针移到 Generator 函数的下一步(gen.next 方法
),然后判断 Generator 函数是否结束(result.done 属性
),如果没结束,就将 next
函数再传入 Thunk 函数(result.value 属性
),否则就直接退出。
函数gen
封装了 n 个异步的读取文件操作,只要执行 run
函数,这些操作就会自动完成。
CO co
The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var gen = function * ( ) { var f1 = yield readFile('/etc/fstab' ); var f2 = yield readFile('/etc/shells' ); console .log(f1.toString()); console .log(f2.toString()); }; var co = require ('co' );co(gen); co(gen).then(function ( ) { console .log('Generator 函数执行完成' ); })
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 var fs = require ('fs' );function readFile (path, cb ) { fs.readFile(path, {encoding : 'utf8' }, cb); } readFile('a.js' , function (err, dataA ) { console .log(dataA); readFile('b.js' , function (err, dataB ) { console .log(dataB); readFile('c.js' , function (err, dataC ) { console .log(dataC); ... }); }); }); ---------- var fs = require ('fs' );var co = require ('co' );function readFile (path ) { return function (cb ) { fs.readFile(path, {encoding : 'utf8' }, cb); }; } co(function * ( ) { var dataA = yield readFile('a.js' ); console .log(dataA); var dataB = yield readFile('b.js' ); console .log(dataB); var dataC = yield readFile('c.js' ); console .log(dataC); }).catch(function (err ) { console .log(err); });
co 将所有 yield
后面的表达式都封装成了 Promise 对象 (本身也返回一个Promise 对象),只有当前表达式执行结束后(即调用 .then
),然后会在 onFulfilled
函数内执行 gen.next(res)
将 res 赋值给 yield
左侧的变量并执行到下一个 yield
,下一个表达式执行结束后又调用 gen.next()
,如此循环,直至 done 变为 true。
ES6 中的 yield
后面可以跟任意类型的值,但 co 对此做了限制,只允许 yield
后跟 thunk
, promise
, generator
, generatorFunction
,array
或者 object
。
原理 co 函数库其实就是将两种自动执行器(Thunk 函数
和 Promise 对象
),包装成一个库。
回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
使用 co 的前提条件是,Generator 函数的 yield
命令后面,只能是 Thunk
函数或 Promise
对象。
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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); } }); } 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) + '"' )); } });
并发 把并发的操作都放在数组或对象 里面, co允许某些操作同时进行,等到它们全部完成,才进行下一步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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);
async async 函数就是 Generator 函数的语法糖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var fs = require ('fs' );var readFile = function (fileName ) { return new Promise (function (resolve, reject ) { fs.readFile(fileName, function (error, data ) { if (error) 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()); }; ----------- var asyncReadFile = async function ( ) { var f1 = await readFile('/etc/fstab' ); var f2 = await readFile('/etc/shells' ); console .log(f1.toString()); console .log(f2.toString()); };
async 函数就是将 Generator 函数的星号(*
)替换成 async
,将 yield
替换成 await
。
优点 (1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
(2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
实现 async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 async function fn (args ) { } ---------- function fn (args ) { return spawn(function *( ) { }); } function spawn (genF ) { return new Promise (function (resolve, reject ) { var gen = genF(); function step (nextF ) { try { var next = nextF(); } catch (e) { return reject(e); } if (next.done) { return resolve(next.value); } Promise .resolve(next.value).then(function (v ) { step(function ( ) { return gen.next(v); }); }, function (e ) { step(function ( ) { return gen.throw(e); }); }); } step(function ( ) { return gen.next(undefined ); }); }); }
使用 同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then
方法添加回调函数。 当函数执行的时候,一旦遇到 await
就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function timeout (ms ) { return new Promise ((resolve ) => { setTimeout(resolve, ms); }); } async function asyncPrint (value, ms ) { await timeout(ms); console .log(value) } asyncPrint('hello world' , 50 ); -------------
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await
命令放在 try...catch
代码块中 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async function myFunction ( ) { try { await somethingThatReturnsAPromise(); } catch (err) { console .log(err); } } async function myFunction ( ) { await somethingThatReturnsAPromise().catch(function (err ) { console .log(err); }); }
await 命令只能用在 async 函数之中。
可以使用 Promise.all 方法使多个请求并发执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 async function dbFuc (db ) { let docs = [{}, {}, {}]; let promises = docs.map((doc ) => db.post(doc)); let results = await Promise .all(promises); console .log(results); } async function dbFuc (db ) { let docs = [{}, {}, {}]; let promises = docs.map((doc ) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console .log(results); }