Open musicode opened 9 years ago
本文比较长,我自认为把 co 的原理说的非常清楚了,耐心看完会有收获的。
Koa之所以可以实现同步代码写异步逻辑,核心就是co。
Koa
co
co是个非常精炼的库,没有第三方依赖,200 多行代码完事。
暴露的方法只有两个:
co(gen)传入一个GeneratorFunction或generator,返回一个Promise。
co(gen)
GeneratorFunction
generator
Promise
先看官方示例:
co(function* () { var result = yield Promise.resolve(true); return result; }).then(function (value) { console.log(value); }, function (err) { console.error(err.stack); });
执行GeneratorFunction返回的是generator,也就是说,co内部会做下面这件事:
if (typeof gen === 'function') { gen = gen.apply(ctx, args); }
如果传入的gen不是GeneratorFunction或generator,说明它不具备异步控制能力,因此返回的Promise会直接resolve(gen)。
gen
resolve(gen)
也就是说,可以像下面这样写:
co(1) .then(function (value) { console.log(value === 1); // true });
不过没啥意义,不用co的异步控制能力还用它干毛。
实际上,co使用了 ES6 Generator,来看一个例子:
function* generatorFunction() { console.log(1); var a = yield 'a'; console.log(2); var b = yield 'b'; console.log(3); var c = yield 'c'; return c; } var gen = generatorFunction(); gen.next(); // 打印 1,返回 {value: "a", done: false} gen.next(); // 打印 2,返回 {value: "b", done: false} gen.next(); // 打印 3,返回 {value: "c", done: false} gen.next('d'); // 没有打印,返回 {value: "d", done: true} gen.next(); // 没有打印,返回 {value: undefined, done: true}
gen.next()的返回对象格式如下:
gen.next()
{ done: {boolean}, value: {*} }
done 表示迭代器是否结束,value 表示yield后面语句的返回值。
yield
需要注意的是结束时的情况,如下:
{ done: true, value: 如果有返回值,value 是返回值,否则是 undefined }
Generator的协程特性具体就不说了,相关文章很多。比如:
Generator
这里只说co用到的特性:
gen.next(value)
gen.throw(error)
结合co再来看一个例子:
var fs = require('co-fs'); co(function* () { var content1 = yield fs.readFile(file1); console.log(content1); var content2 = yield fs.readFile(file2); return content2; }) .then(function (value) { console.log(value); });
这里会把 file1 和 file2 的内容依次打印出来。
第一次看到这种写法一定觉得非常神奇,js 居然可以这样写异步逻辑。
下面详细讲解原理。
一个generator的内部可能存在多个yield,即一个yield结束之后会执行下一个yield,这个过程应该是递归的。
我们知道,递归需要两个条件:
对于1来说,是不断的gen.next(),对于2来说,result.done 为 true 就算结束了。
1
2
我们先实现第一版:
function next(result) { if (result) { if (result.done) { return resolve(result.value); } result = gen.next(result.value); } else { result = gen.next(); } result.value.then(function (value) { result.value = value; next(result); }); }
result.value是yield后面语句的同步返回结果,这个语句本身,可以是同步的(如yield 1),也可以是异步的(如yield asyncWork())。
result.value
同步
yield 1
yield asyncWork()
虽然在co中,写成yield 同步实属蛋疼,但作为库还是要兼容的。
yield 同步
co推荐yield后面的语句返回Promise,当然你也可以用回调神马的,但是官方不保证会一直支持下去,因此我们就按一切皆 Promise来讲解。
一切皆 Promise
Promise规范有个术语叫value,一切语法正确的值都可以是value,包括undefined。这个规定和gen.next()返回值中的 value 特别 match,两者的结合真是上天注定的缘份。
术语
value
undefined
我们改写一段next()中的代码:
next()
var promise = result.value; if (!promise || typeof promise.next !== 'function') { promise = new Promise(function (resolve) { resolve(result.value); }); }
这样便保证 value 一定是Promise,接下来只需要静候Promise转换状态即可。
promise.then(onFulfilled, onRejected);
Promise规范规定,操作成功时,执行onFulfilled回调函数,它的第一个参数是value;操作失败时,执行onRejected回调函数,它的第一个参数是reason。
onFulfilled
onRejected
reason
递归函数next的优化版如下:
next
function next(result) { if (result) { if (result.done) { return resolve(result.value); } result = gen.next(result.value); } else { result = gen.next(); } var promise = result.value; if (!promise || typeof promise.next !== 'function') { promise = new Promise(function (resolve) { resolve(result.value); }); } promise.then(onFulfilled, onRejected); }
最后一句为什么不写成下面这样呢?
promise.then( function (value) { result.value = value; next(result); }, function (reason) { // 暂不实现 } );
我的猜测是递归调用会创建太多函数,影响内存(如果不是,请告诉我为啥不这么写...)。
如果把onFulfilled和onRejected定义为外部函数,它们三个的调用关系需要探讨一下。
function next(result) {} function onFulfilled(value) {} function onRejected(reason) {}
next肯定不能作为入口函数,因为onFulfilled只有一个 value 参数,没有办法传入 done。
onRejected作为入口函数实在太搞笑了,好吧,onFulfilled几乎就是命中注定的。
基于yield后面跟一个Promise的共识,下面来看一个读取文件的例子:
co(function* () { var content = yield readFile(file); });
本例中 promise 的逻辑是读取文件,假设 readFile 函数的实现如下:
var fs = require('fs'); function readFile() { var args = [].slice.call(arguments); return new Promise(function (resolve, reject) { args.push(function (err, data) { if (err) { reject(err); } else { resolve(data); } }); fs.readFile.apply(fs, args); }); }
文件读取成功会执行onFulfilled函数,实现如下:
function onFulfilled(value) { var result = gen.next(value); next(result); }
作为入口函数来说,不传参数也是正常的。
考虑到调用gen.next()可能会报错(这里指的是业务代码,谁能保证不报错...),因此最好 try 一下:
function onFulfilled(value) { var result; try { result = gen.next(value); } catch (e) { return reject(e); } next(result); }
读取成功的逻辑到这就算说清楚了。
下面再说文件读取失败的情况。
我们知道,异步的失败是 catch 不到的,比如:
try { var promise = new Promise(function (resolve) { setTimeout( function () { throw new Error('haha'); } ); }); } catch (e) { console.log('[error]', e); }
结果并不会打印[error]。
[error]
如果要 catch 异步失败,try 和逻辑之间必须是同步执行的关系,如下:
function asyncWork() { } setTimeout( function () { try { asyncWork(); } catch (e) { console.log('[error]', e); } } );
co没有这么麻烦,因为 ES6 Generator 提供了一种全新的错误处理方式,如下:
function* generatorFunction(x) { var y; try { y = yield x + 1; } catch (e) { console.log(e); } return y; } var gen = generatorFunction(1); gen.next(); // {value: 2, done: false} gen.throw('error');
generator中的业务逻辑一定要考虑失败的情况,比如读取文件,读取失败还怎么赋值给 content 变量呢?
如果yield后跟一段同步逻辑,try...catch 可以正常捕获;如果yield后跟一段异步逻辑,try...catch 就失效了。
异步失败需要通知给generator,gen.throw()就有能力做到。
gen.throw()
需要注意的是,如果generator内部没有对应的 try...catch 捕获错误,gen.throw()会导致程序报错,相当于throw new Error('xxx')。
throw new Error('xxx')
下面是onRejected函数的实现:
function onRejected(reason) { var result; try { result = gen.throw(reason); } catch (e) { return reject(e); } next(result); }
传入一个generator,返回一个普通函数,这个函数会返回一个Promise。
看一个官方示例:
var fn = co.wrap(function* (val) { return yield Promise.resolve(val); }); fn(true).then(function (val) { });
wrap其实非常简单,几乎就是下面这样的:
wrap
function wrap(fn) { return function () { return co.call(this, fn.apply(this, arguments)); }; }
真相就是函数 curry 化,换种方式执行co。
co.call 还是不太明白
Koa
之所以可以实现同步代码写异步逻辑,核心就是co
。co
是个非常精炼的库,没有第三方依赖,200 多行代码完事。暴露的方法只有两个:
co(gen)
co(gen)
传入一个GeneratorFunction
或generator
,返回一个Promise
。先看官方示例:
执行
GeneratorFunction
返回的是generator
,也就是说,co
内部会做下面这件事:如果传入的
gen
不是GeneratorFunction
或generator
,说明它不具备异步控制能力,因此返回的Promise
会直接resolve(gen)
。也就是说,可以像下面这样写:
不过没啥意义,不用
co
的异步控制能力还用它干毛。实际上,
co
使用了 ES6 Generator,来看一个例子:gen.next()
的返回对象格式如下:done 表示迭代器是否结束,value 表示
yield
后面语句的返回值。需要注意的是结束时的情况,如下:
Generator
的协程特性具体就不说了,相关文章很多。比如:这里只说
co
用到的特性:gen.next()
和yield
配合使用,以yield
为界分段执行gen.next(value)
用异步操作结果(value)改写前一个yield
的 valuegen.throw(error)
抛出错误可在generator
内部捕获结合
co
再来看一个例子:这里会把 file1 和 file2 的内容依次打印出来。
第一次看到这种写法一定觉得非常神奇,js 居然可以这样写异步逻辑。
下面详细讲解原理。
一个
generator
的内部可能存在多个yield
,即一个yield
结束之后会执行下一个yield
,这个过程应该是递归的。我们知道,递归需要两个条件:
对于
1
来说,是不断的gen.next()
,对于2
来说,result.done 为 true 就算结束了。我们先实现第一版:
result.value
是yield
后面语句的同步
返回结果,这个语句本身,可以是同步的(如yield 1
),也可以是异步的(如yield asyncWork()
)。虽然在
co
中,写成yield 同步
实属蛋疼,但作为库还是要兼容的。co
推荐yield
后面的语句返回Promise
,当然你也可以用回调神马的,但是官方不保证会一直支持下去,因此我们就按一切皆 Promise
来讲解。Promise
规范有个术语
叫value
,一切语法正确的值都可以是value
,包括undefined
。这个规定和gen.next()
返回值中的 value 特别 match,两者的结合真是上天注定的缘份。我们改写一段
next()
中的代码:这样便保证 value 一定是
Promise
,接下来只需要静候Promise
转换状态即可。递归函数
next
的优化版如下:最后一句为什么不写成下面这样呢?
我的猜测是递归调用会创建太多函数,影响内存(如果不是,请告诉我为啥不这么写...)。
如果把
onFulfilled
和onRejected
定义为外部函数,它们三个的调用关系需要探讨一下。next
肯定不能作为入口函数,因为onFulfilled
只有一个 value 参数,没有办法传入 done。onRejected
作为入口函数实在太搞笑了,好吧,onFulfilled
几乎就是命中注定的。基于
yield
后面跟一个Promise
的共识,下面来看一个读取文件的例子:本例中 promise 的逻辑是读取文件,假设 readFile 函数的实现如下:
文件读取成功会执行
onFulfilled
函数,实现如下:作为入口函数来说,不传参数也是正常的。
考虑到调用
gen.next()
可能会报错(这里指的是业务代码,谁能保证不报错...),因此最好 try 一下:读取成功的逻辑到这就算说清楚了。
下面再说文件读取失败的情况。
我们知道,异步的失败是 catch 不到的,比如:
结果并不会打印
[error]
。如果要 catch 异步失败,try 和逻辑之间必须是同步执行的关系,如下:
co
没有这么麻烦,因为 ES6 Generator 提供了一种全新的错误处理方式,如下:generator
中的业务逻辑一定要考虑失败的情况,比如读取文件,读取失败还怎么赋值给 content 变量呢?如果
yield
后跟一段同步逻辑,try...catch 可以正常捕获;如果yield
后跟一段异步逻辑,try...catch 就失效了。异步失败需要通知给
generator
,gen.throw()
就有能力做到。需要注意的是,如果
generator
内部没有对应的 try...catch 捕获错误,gen.throw()
会导致程序报错,相当于throw new Error('xxx')
。下面是
onRejected
函数的实现:co.wrap(gen)
传入一个
generator
,返回一个普通函数,这个函数会返回一个Promise
。看一个官方示例:
wrap
其实非常简单,几乎就是下面这样的:真相就是函数 curry 化,换种方式执行
co
。