首页

Published

- 8 min read

co库核心原理解析与实现

img of 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 语法结构极为相似,值得学习!