Node.js
中的 co
模块主要用于 Generator
函数的自动执行,可以使我们以同步的形式编写异步代码
实例一
先来看两个对比实例,传统方式下,sayhello
是一个异步函数,执行 helloworld
会先输出 'world'
再输出 'hello'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| setTimeout(() => { console.log('!') }, 0)
function sayhello() { return Promise.resolve('hello').then(function (hello) { console.log(hello) }) }
function helloworld() { sayhello() console.log('world') }
helloworld()
|
这是因为 Promise
是基于任务队列机制的(详细可以参考 JavaScript 并发模型),即当前代码执行完的时候才会触发,但是会在下一个 EventLoop
之前执行(注意与 setTimeout
区分开来)
实例二
我们将上面的示例换一种写法,调整成 Promise + Generator
的方式来试试,也就是模拟一下 co
当中的实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function co(gen) { var it = gen() var ret = it.next() ret.value.then(function (res) { it.next(res) }) }
function sayhello() { return Promise.resolve('hello').then(function (hello) { console.log(hello) }) }
co(function* helloworld() { yield sayhello() console.log('world') })
|
我们模拟实现了 co
函数,首先生成一个迭代器,然后执行一遍 next()
,得到的 value
是一个 Promise
对象,promise.then()
里面再执行 next()
,运行后可以发现,结果就是我们想要的先输出 'hello'
再输出 'world'
从上面示例可以看出,Generator
函数体可以挂载在 yield
语句处,直到下一次执行 next()
,我们本章当中将要介绍的 co
模块的思路也就是利用了 Generator
的这个特性,将异步操作跟在 yield
后面,当异步操作完成并返回结果后,再触发下一次 next()
,当然,跟在 yield
后面的异步操作需要遵循一定的规范 thunks
和 promises
从上面示例我们也可以简单的推算出 co
的主要功能有下面这些
- 异步流程控制,依次执行
Generator
函数内的每个位于 yield
后的 Promise
对象,并在 Promise
的状态改变后,把其将要传递给 reslove
函数的结果或传递给 reject
函数的错误返回出来,可供外部来进行传递值等操作,这些 Promise
是串行执行的
- 若
yield
后是 Promise
对象的数组或属性值是 Promise
对象的对象,则返回出结构相同的 Promise
执行结果数组(对象),并且这些 Promise
是并行执行的
co
自身的返回值也是一个 Promise
对象,可供继续使用
run
由上面的示例我们可以发现,Generator
函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权,有两种方法可以做到这一点
- 回调函数,将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权
Promise
对象,将异步操作包装成 Promise
对象,用 then
方法交回执行权
在看 co
源码之前,我们先来尝试着自己实现一下,也就是稍微完善一下上面的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function run(gen) { var gen = gen() function next(data) { var result = gen.next(data) if (result.done) return if (isPromise(result.value)) { result.value.then(data => { next(data) }) } else { result.value(next) } } next() }
function isPromise(obj) { return typeof obj.then == 'function' }
|
上面我们已经完成了一个基本版的启动器函数,支持 yield
后跟回调函数或者 Promise
对象,但是并不完善,比如我们没有针对 Generator
进行错误捕获,所以我们可以考虑将其封装成一个 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 39 40 41 42
| function run(gen) { var gen = gen() return new Promise((resolve, reject) => { function next(data) { try { var result = gen.next(data) } catch (e) { return reject(e) } if (result.done) { return resolve(result.value) } var value = toPromise(result.value) value.then(data => { next(data) }, e => { reject(e) }) } next() }) }
function isPromise(obj) { return typeof obj.then == 'function' }
function toPromise(obj) { if (isPromise(obj)) return obj if (typeof obj == 'function') return thunkToPromise(obj) return obj }
function thunkToPromise(fn) { return new Promise(function (resolve, reject) { fn(function (err, res) { if (err) return reject(err) resolve(res) }) }) }
|
在这一版当中,我们返回了一个 Promise
- 当
result.done
为 true
的时候,我们将该值 resolve(result.value)
- 如果执行的过程中出现错误,被
catch
住,我们会将原因 reject(e)
- 其次,我们会使用
thunkToPromise
将回调函数包装成一个 Promise
,然后统一的添加 then
函数
最后,我们再来看看 co
源码当中具体是如何实现的
co
源码实现如下
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function (resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
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 onRejected(err) { var ret; try { ret = gen.throw(err); } 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) + '"')); } }); }
|
核心代码入口是 onFulfilled
,无论如何第一次的 next(ret)
是一定要执行的,因为 generator
必须要 next()
一下的,但是 co
实际上有两种调用方式,分为有参数和无参数的,很明显以上是无参数的 generator
执行器,那么有参数的 wrap
呢?co
为我们提供了简单的包装
1 2 3 4 5 6 7 8 9
| co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } };
|
通过 call
和 apply
的组合使用来传递 arguments
辅助函数
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
function arrayToPromise(obj) { return Promise.all(obj.map(toPromise, this)); }
function thunkToPromise(fn) { var ctx = this; return new Promise(function (resolve, reject) { fn.call(ctx, function (err, res) { if (err) return reject(err); if (arguments.length > 2) res = slice.call(arguments, 1); resolve(res); }); }); }
function objectToPromise(obj) {
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; }
return Promise.all(promises).then(function () { return results; });
function defer(promise, key) { results[key] = undefined; promises.push(promise.then(function (res) { results[key] = res; })); } }
|
经过上面这些步骤,我们可以得到 yield
后面只能是函数、Promise
对象、Generator
函数、Generator
迭代器对象、数组(元素仅限之前的 4
类)和 Object
(对应 value
仅限定之前的 4
类),现在可以把 co
串行调用 generator
函数中 yield
的过程总结如下
- 首先进入最外层的
Promise
- 通过入口
onFilfilled()
方法,将 generator
函数运行至第一个 yield
处,执行该 yield
后边的异步操作,并将结果传入 next
方法
- 如果
next
中传入结果的 done
为 true
(已经完成),则返回最外层 Promise
的 resolve
- 如果
next
中传入结果的 done
为 fasle
(表示还没执行完),则返回 value
(即 yield
后边的对象)然后查看是否可以转化为内部 Promise
对象。如无法转化则抛出错误,返回最外层 Promise
的 reject
- 若能转化为
Promise
对象,则通过 then(onFilfilled, onRejected)
开始执行
- 在
onFilfilled()
或者 onRejected()
内部调用再次调用 next()
方法,实现串行执行 yield
,并将 yield
后边的对象传递给 next()
,依次重复(实现链式调用)
- 所有
yield
执行返回,将最后的 return
值返回给最外层 Promise
的 resovle
方法,结束 co
对 generator
函数的调用
参考