在前面的章节中我们通过 Generator 体验了一种全新的异步流程书写方式, 我们在异步操作前面加上 yield 命名,通过promise对异步状态的绑定和指针就能控制程序是否交出执行权。
但是我们也发现了一些问题:
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);
});
如果 Generator 函数中存在多个异步操作,我们必须通过next方法,手动的去改变内部指针。 这个过程会非常麻烦,因此我们基于thunk 函数的思想封装了自动执行器,当异步操作有了结果,自动交回执行权。
CO模块用法
比如,有一个 Generator 函数,用于依次读取两个文件。
var gen = function* (){
var f1 = yield readFile('/src/home');
var f2 = yield readFile('/src/route');
console.log(f1.toString());
console.log(f2.toString());
co 函数库可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
})
上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
co 函数库的原理
为什么 co 可以自动执行 Generator 函数?
前面文章说过,Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。 (2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。
注意:co官方是建议yield后面跟Promise的,虽然支持thunk,但是未来可能会移除。
基于 Promise 对象的自动执行
还是沿用上面的例子。首先,把 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) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/src/home');
var f2 = yield readFile('/src/route');
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);
上面代码中,只要 Generator 函数还没执行到最后一步,next 函数就调用自身,以此实现自动执行。
co 函数库的源码
co 就是上面那个自动执行器的扩展,它的核心源代码只有几十行,非常简单。
首先,co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。
var slice = Array.prototype.slice;
function co(gen) {
// 保存当前的执行环境
var ctx = this;
// 切割出函数调用时传递的参数
var args = slice.call(arguments, 1);
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 方法,包装成 onFulefilled 函数。这主要是为了能够捕捉抛出的错误。
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);
}
});
}
这里要特别注意:onFulfilled 是会接收一个参数res, 但是我们在首次调用onFulfilled时是并没有传递任何参数的,原因是因为我们刚刚讲到了 onFulfilled 封装了 Generator 函数的内部指针对象的 next 方法, 当我们首次调用next方法的时候移动指针指向下一阶段是不需要对他传参的。 只有在执行第二及以上阶段时才需要传参。 next(res)传入的参数会做为此阶段yield语句的返回结果像Generator 函数内输入。
此时的 ret 是获取第一个阶段执行的结果值是一个对象,表示当前阶段的信息 这个对象中有两个属性 value 和 done 属性,这个我们之前讲过它是 Generator 函数向外输出数据的一种方式。
最后,就是关键的 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 函数。 第四行,在参数不符合要求的情况下(参数非 数组/对象 和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
第三点的onRejected封装在哪呢?
function onRejected(err) {
var ret;
try {
// 尝试抛出错误
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
// 处理结果
next(ret);
}
并发的异步操作
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);
对于以上代码中的onFulfilled和onRejected,我们可以把它们看成是co模块对于resolve和reject封装的加强版。
第二,参数Promise化,我们来看一下co中的toPromise的实现:
1
2
3
4
5
6
7
8
9
10
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;
}
toPromise的本质上就是通过判定参数的类型,然后再通过转移控制权给不同的参数处理函数,从而获取到期望返回的值。
关于参数的类型的判断,看一下源码就能理解了,比较简单。
先来看下arrayToPromise的实现:
1
2
3
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。 this 可选。对象作为该执行回调时使用,传递给函数,用作 “this” 的值。
如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
//相当于
[Promise.resolve(1), Promise.resolve(2)].map(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;
}, this);
我们着重来分析一下objectToPromise的实现:
假如我们这样使用co模块:
1
2
3
4
5
6
7
8
// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
源码处理方式:
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
function objectToPromise(obj){
// 获取一个和传入的对象一样构造器的对象
var results = new obj.constructor();
// 获取对象的所有可以遍历的key
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// 对于数组的每一个项都调用一次toPromise方法,变成Promise对象
var promise = toPromise.call(this, obj[key]);
// 如果里面是Promise对象的话,则添加一个回调等异步操作成功取出resolved后的值 添加到results中
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
// 并行,按顺序返回结果,返回一个数组
return Promise.all(promises).then(function () {
return results;
});
// 根据key来获取Promise实例resolved后的结果,
// 从而push进结果数组results中
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
Promise.all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
p的状态由p1、p2、p3决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
示例代码:
1
2
3
4
5
6
7
8
9
10
11
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return Promise.resolve(id);
});
Promise.all(promises).then(function (value) {
console.log(value);
}).catch(function(reason){
// ...
});
//打印结果: [2, 3, 5, 7, 11, 13]
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
参考地址:
阮一峰 https://www.ruanyifeng.com/blog/2015/05/co.htmlES6 https://es6.ruanyifeng.com/#docs/generator-async https://juejin.cn/post/6844904133577670664#heading-9 https://segmentfault.com/a/1190000011802111