这是我参与更文挑战的第8天,活动详情查看:更文挑战
对 Koa 框架有了解的同学应该知道,Koa1 中间件使用 Generator 函数,Koa2 的中间件使用 async 函数。但为了兼容 Generator 函数,Koa2 使用了 co函数库,来将 Generator 转成 Promise。
基本概念
在分析 co 源码之前,我们先简单了解一些基本的知识。
Generator 函数与 Generator 对象
Generator 函数由 function*语法定义,简单使用如下所示:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
运行这种函数会返回一个 Generator 对象,
var hw = helloWorldGenerator();
console.log(hw.__proto__); // Generator {}
可以看到 Generator 函数里有 yield 表达式,而 Generator 对象继承了 next、return、throw 方法。Generator 对象每次执行 next 方法,就会从 Generator 函数上一次停下了的地方继续执行到下一个 yield 或者是 return 处,yield(或 return)后面的表达式是返回值。next 方法返回一个{done:..., value:...}结构的对象,value 是 yield 或者 return 表达式的返回值,如下所示:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
详细内容可以参看 Generator 函数的语法 和 Generator 函数的异步应用。
Promise
Promise 对象可以看成是一个容器,封装了某个未来才会结束的事件(通常是一个异步操作)。Promise 具有三种状态:
- pending(未完成)
- fulfilled(成功)
- rejected(失败)
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。简单使用如下所示:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
当调用 resolve 函数时,Promise 会从 pending 状态变为 fulfilled 状态;调用 reject 函数时,Promsie 会从 pending 状态变为 rejected 状态。
可以用 then 方法指定当 Promise 的状态改变时要执行的回调函数:
promise.then(function(value) {
// Promise 状态变为 fulfilled 时调用
}, function(error) {
// Promise 状态变为 rejected 时调用
});
除了 then 方法,Promise 还有 catch 和 finally 方法:
promise
.then(result => {···}) // Promise 状态变为 fulfilled 时调用
.catch(error => {···}) // Promise 状态变为 rejected 时调用
.finally(() => {···}); // 无法知道上一阶段状态,都会调用
简单来说,Promise 对象将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。可以翻阅 Promise 对象 进行更加深入的学习。
Thunk 函数
Thunk 函数,是指只接收一个回调函数作为参数的函数。示例如下所示:
function sum (x, y, callback) {
setTimeout(function () {
callback(x + y)
}, 1000)
}
// 固定前几个参数值,
// 将 foo 封装成一个 Thunk 函数,只需要再接收一个回调函数作为参数
function sumThunk (callback) {
return sum(3, 4, callback)
}
// 执行 Thunk 函数
sumThunk(function (sum) {
console.log(sum)
})
更加详细的内容可以查看 You-Dont-Know-JS Thunks 。
源码分析
终于到了源码环节。co 源码代码量其实很小,只有一个 index.js 文件,核心代码只有几十行,学习起来并不需要花费很多时间。这也算是我第一个逐行看完源码的项目了。
类型判断
在分析核心代码之前,我们先学习一下源码中的一些类型判断方法,看大佬是怎么做类型判断的。在 co 中封装了 promise、普通对象、Generator 函数和 Generator 对象的类型判断方法,如下所示:
// 判断 obj 是否是 promise
function isPromise(obj) {
return 'function' == typeof obj.then;
}
// 判断 val 是否是普通对象
function isObject(val) {
return Object == val.constructor;
}
// 判断 obj 是否是一个 Generator 函数
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
// 判断 obj 是否是一个 Generator 对象
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
核心逻辑
下面我们来看一下 co 的核心代码:co 函数,最后导出供我们使用的就是这个函数。该函数接受一个 Generator 函数,返回一个 Promise 对象。
co 首先判断传递的参数是否是函数类型,如果是就执行这个函数获得返回值,然后判断返回值是否是一个 Generator 对象。如果参数不符合条件就将 Promise 对象的状态改为 fulfilled 。
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);
// 判断是否是 Generator,如果不是就直接返回,将 Promise 状态置为 fulfilled 状态
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
// ......
});
}
如果参数满足条件,就运行 onFulfilled 函数。
onFulfilled执行 Generator 对象的 next 方法,如果发生异常就直接将 Promise 的状态改为 reject。
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
获取到 Generator 对象的 next 方法的返回值,调用自定义的 next 函数。
next 函数对 ret 进行校验,将 value 转换成 Promise,再通过 Promise.then 方法继续执行 onFulfilled 继续执行 Generator 对象的 next 方法,从而实现 Generator 函数自执行,具体每一步的解释见注释如下:
function next(ret) {
// 判断当前是否为 Generator 函数最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 将返回值转换成 Promise
var value = toPromise.call(ctx, ret.value);
// 使用 promise 的 then 方法,通过 onFulfilled 函数再次调用 next 函数,实现 Generator 函数的自执行
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 如果迭代器的 value 无法转换成 Promise,说明传入的 Generator 函数不满足条件,将 Promise 对象的状态改为 rejected,终止执行。
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 库实现 Generator 自执行的逻辑:
- 调用 onFulfilled 函数执行迭代器的 next 方法;
- 调用 next 函数将迭代器的 value 转换成 Promise,通过 Promise.then 方法再执行 onFulfilled 函数
- 不断重复步骤 1 和 2,直到 Generator 函数已经执行完毕,得到最后返回值的 value,将 Promise 的状态改为 resolve。
异常处理和 Promise 转换
最后再看一下 onRejected 函数跟 toPromise 函数。
onRejected 函数调用 Generator 对象的 throw抛出错误,该错误会优先被 Generator 函数内部的 try...catch 捕获,然后继续执行 next函数进行返回值的判断,继续自执行逻辑。 如果 Generator 函数内部没有 try...catch代码块,则该异常会被外部的try...catch捕获,将 Promise 的状态置为 rejected。
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
toPromise 函数将执行 Generator 所返回的 value 转换成 Promise 对象,具体逻辑如下所示:
function toPromise(obj) {
// 假值或者 Promise 没有处理必要,直接返回
if (!obj) return obj;
if (isPromise(obj)) return obj;
// 如果是 Generator 函数或者 Generator 对象,递归调用 co 函数进行转化
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// 如果是 thunk 函数、数组、对象,调用相应方法进行转化
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;
}
Thunk 函数转换成 Promise 的逻辑如下:
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);
});
});
}
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。这种使用方式,需要将并发的操作放在数组或者对象里面。
传入数组的情况,需要将数组各项 Generator 转换成 Promise,逻辑如下:
function arrayToPromise(obj) {
// 回忆一下,toPromise 里,如果入参是 Generator,调用 co 函数实现 promise 转换
return Promise.all(obj.map(toPromise, this));
}
传入对象的情况,处理逻辑如下:
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];
// 将对象的每个属性都进行 Promise 转换
var promise = toPromise.call(this, obj[key]);
// 如果是 promise,则将其放入 promises 数组
if (promise && isPromise(promise)) defer(promise, key);
// 如果不是,放入结果数组里
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
co.wrap
最后,再补充一个 co.wrap 方法,类似于函数柯里化的功能,先将 Generator 函数传入 co,后续获取到所需参数后再真正执行 promise 的转换,用例如下:
// 实际返回一个柯里化后的函数
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
// 传入参数后才返回 promise
fn(true).then(function (val) {
// do something...
});
实现该方法的源码及解释如下所示:
co.wrap = function (fn) {
// 将 fn 存起来,可能某些情况需要重新拿到这个 Generator Function
createPromise.__generatorFunction__ = fn;
// 封装了传入的 fn
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
总结
co 库用于将 Generator 转换成 Promise,实现 Generator 自执行的核心逻辑如下:
- 调用 onFulfilled 函数执行迭代器的 next 方法;
- 调用 next 函数将迭代器的 value 转换成 Promise,通过 Promise.then 方法再执行 onFulfilled 函数
- 不断重复步骤 1 和 2,直到 Generator 函数已经执行完毕,得到最后返回值的 value,将 Promise 的状态改为 resolve。
co 接受的 Generator 函数,yield 的返回值( yieldable objects)需要是以下几种之一:
- promise
- Thunk 函数
- 数组
- 对象
- Generator 对象
- Generator 函数
处理异常时,优先由 Generator 函数的try...catch代码块捕获,没有捕获的会被外部try...catch代码块捕获。
思考一下
下列代码在源码中是怎么执行的?
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
参考
co 函数库的含义和用法,by 阮一峰
Thunk 函数的含义与用法,by 阮一峰
co 源码,by TJ Holowaychuk
Generator,by MDN
You-Dont-Know-JS Thunks,by JobbyM