Published
- 8 min read
co库核心原理解析与实现
前言
co 是一个基于 Generator 实现的控制流编排库,能让开发者以串行的方式编写非阻塞的逻辑。
co 库诞生于 2013 年,在那个时候连 Promise 还没有正式进入 ECMAScript 标准,更不用提 async/await 语法了,那时候普遍采用回调的方式实现异步流程交互逻辑。然而,这种方式非常容易使代码陷入“回调地狱”,使得代码逻辑混乱、难以维护:
function f1(callback) {
// do something
// ....
callback(err, result)
}
function f2(callback) {}
function f3(callback) {}
f1((e1, r1) => {
f2((e2, r2) => {
f3((e3, r3) => {
// 回调地狱!
})
})
})
co 库的出现为这种情况带来了一个新的解决方案,它巧妙地利用了生成器的 yield 语法使开发者能以串行的顺序编写异步逻辑:
function f1(args) {
return function (callback) {}
}
function f2(args) {
return new Promise(function (resolve, reject) {
// do something
// ....
resolve(result)
})
}
function* f3() {
var a = yield f1(args)
var b = yield f2(args)
return [a, b]
}
// co 实际上是一个用于驱动生成器执行的方法
// 它在内部处理了等待异步回调的流程,并返回 Promise 结果
co(f3()).then((result) => console.log(result))
核心原理解析
正如 前言 章节所提到的,co 库提供的实际上是一个用于驱动生成器执行的方法,其核心原理在于实现执行到异步流程时“暂停”执行生成器,异步流程结束时“继续”执行生成器的逻辑。
生成器简介
生成器 是由 生成器函数 返回的可迭代对象,支持惰性求值、中断执行、恢复执行等特性。其中,生成器的中断执行以及恢复执行特性是 co 库功能实现的核心依赖。
生成器对象只能通过调用生成器函数获取,生成器函数通过 function* 关键字定义,其内部可通过 yield 关键字产生一个值并让出执行空间,外部可通过 generator.next() 继续执行生成器并获取其产生的值,以下是一个简单示例:
// 定义生成器函数
function* f() {
console.log('f() - 1')
var a = yield 1
console.log('f() - 2')
var b = yield 2
console.log('f() - 3')
return a + b
}
// 获取生成器对象
var g = f()
console.log(g.next())
// 输出 'f() - 1'
// 输出 { value: 1, done: false }
console.log(g.next(1))
// 输出 'f() - 2'
// 输出 { value: 2, done: false }
// 外部也可通过 `g.throw()` 或 `g.return()` 中断生成器的执行
// g.throw(new Error('stop'))
// g.return(null)
console.log(g.next(2))
// 输出 'f() - 3'
// 输出 { value: 3, done: true }
// 返回 done === true 则表示生成器已执行结束
co 库核心功能实现
从以上对生成器的简介中,我们可以猜想出 co 库驱动生成器执行的大致流程:
// 1. 开始执行生成器
co(g)
// 2. 获取生成器产生的值(异步任务 - Promise/thunk函数/生成器等)
var r = g.next()
// 3. 等待异步任务执行完成后再继续执行生成器
// r.value = function (callback) {}
r.value(function (err, result) {
if (err) {
return g.throw(err)
}
g.next(result)
})
// 4. 重复以上 2-3 流程,直至生成器执行完毕
为了方便读者理解,以下会从基础版本开始实现 co 库并逐步完善功能。
注:实际上,现在基本上已经不再需要通过 co 库来处理异步任务了,为了匹配当时实现的背景,以下采用 ES5 语法对功能进行实现,并假设当前环境已引入 Promise polyfill。
基础版本
基础版本仅考虑支持 Promise 类型异步任务,对于任何其他类型均直接以 Promise 包裹返回:
function co(g) {
function step(result) {
var promise = toPromise(result.value)
if (result.done) {
return promise
}
return (
promise
// 等待 Promise 执行完毕后继续执行生成器
.then(next, onThrow)
)
}
function next(value) {
return step(g.next(value))
}
function onThrow(e) {
return step(g.throw(e))
}
return new Promise(function (resolve) {
resolve(next())
})
}
function isPromise(o) {
return !!o && typeof o === 'object' && typeof o.then === 'function'
}
function toPromise(o) {
if (isPromise(o)) {
return o
}
return new Promise(function (resolve) {
resolve(o)
})
}
支持 thunk 函数
注:thunk 函数是指仅接收一个 callback(error, result) 回调函数作为参数的普通函数。
由于以上 co() 函数主体实现以 Promise 为基础驱动生成器执行,因此,新增支持类型无需改动 co() 函数,仅需改动 toPromise() 函数。
function isFunction(o) {
return typeof o === 'function'
}
function thunkToPromise(f) {
return new Promise(function (resolve, reject) {
f(function (error, result) {
if (error) {
return reject(error)
}
resolve(result)
})
})
}
function toPromise(o) {
if (isFunction(o)) {
return thunkToPromise(o)
}
// ....
}
支持生成器
对于采用 co 生态实现的异步功能模块,开发者通常需要把主体功能划分为多个子模块实现,因此支持生成器嵌套也是常见且必须支持的需求。在实现上,仅需递归调用 co() 函数即可。
function isGenerator(o) {
return (
!!o && typeof o === 'object' && typeof o.next === 'function' && typeof o.throw === 'function'
)
}
function toPromise(o) {
if (isGenerator(o)) {
return co(o)
}
// ....
}
完整实现
function co(g) {
function step(result) {
var promise = toPromise(result.value)
if (result.done) {
return promise
}
return (
promise
// 等待 Promise 执行完毕后继续执行生成器
.then(next, onThrow)
)
}
function next(value) {
return step(g.next(value))
}
function onThrow(e) {
return step(g.throw(e))
}
return new Promise(function (resolve) {
resolve(next())
})
}
function isPromise(o) {
return !!o && typeof o === 'object' && typeof o.then === 'function'
}
function isFunction(o) {
return typeof o === 'function'
}
function isGenerator(o) {
return (
!!o && typeof o === 'object' && typeof o.next === 'function' && typeof o.throw === 'function'
)
}
function thunkToPromise(f) {
return new Promise(function (resolve, reject) {
f(function (error, result) {
if (error) {
return reject(error)
}
resolve(result)
})
})
}
function toPromise(o) {
if (isPromise(o)) {
return o
}
if (isFunction(o)) {
return thunkToPromise(o)
}
if (isGenerator(o)) {
return co(o)
}
return new Promise(function (resolve) {
resolve(o)
})
}
结语
本文介绍并实现了 co 库的核心功能,虽然如今基本上已经不再需要通过 co 库来编排异步任务,但其设计思想在今天来看仍是超前的,在 co 生态下编写的异步模块语法与现在的 async/await 语法结构极为相似,值得学习!