skip to content
FaiChou's blog

co函数的理解

/ 4 min read

tj/co 是一个 generator 自执行的状态管理库, 短短的几百行代码即收获了 11k 的 star, 其实 generator 自执行也是 redux-saga 的核心, 精简代码如下:

function co(fn) {
return function() {
var gen = fn.apply(this, arguments)
function handle(result) {
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value)
.then(function(res) {
return handle(gen.next(res))
})
.catch(function(ex) {
return handle(gen.throw(ex))
})
}
try {
var result = gen.next()
return handle(result)
} catch (ex) {
return Promise.reject(ex)
}
}
}

首先配合一个例子来解释下它的作用:

function* login(name, pswd, session) {
var user = yield queryUser(name);
var hash = yield crypto.hashAsync(pswd + user.salt);
if (user.hash !== hash) {
throw new Error('Incorrect password');
}
session.setUser(user);
}

这是一个简单的登录的例子, 有两个异步方法: queryUsercrypto.hashAsync, 我们知道 yield 关键字相当于 return 会将表达式返回, 并且要继续往下执行, 需要一个 for of 或者手动调用 .next(), 所以这里可以配合 co 这个函数来自动执行它.

例子可以这样使用: co(login)(), 其中 co 接受一个 generator 并返回一个函数, 这个函数的执行会自动执行里面被 yield 的方法. co 的核心是 handle 方法:

function handle(result) {
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value)
.then(function(res) {
return handle(gen.next(res))
})
.catch(function(ex) {
return handle(gen.throw(ex))
})
}

首先它需要判断 generator 结束与否, 使用 result.done 来判断. 如果没有结束, 此时 result.value 即为yield 的异步方法, 所以需要 Promise.resolve(result.value) 来执行. 执行结束, 拿到结果继续递归调用 handle 方法来向下执行, 因为需要将异步方法的结果传递下去, 需要将结果传递给 next: return handle(gen.next(res)).

有一个难点是 catch 块, 这里面为什么需要再次调用 handle? 直接 throw 会有什么问题吗?

这里我再写个例子来说明下:

var delayMsg = (ms, content) => new Promise(r => setTimeout(r, ms, content))
var delayErr = (ms, content) => new Promise((_, reject) => setTimeout(reject, ms, new Error(content)))
co(function* (value) {
try {
var data1 = yield delayMsg(2000, value)
yield delayErr(2000, data1)
var data2 = yield delayMsg(1000, data1)
console.log(data2)
} catch (error) {
console.log('IN CATCH BLOCK:', error)
yield 1
}
})('hello')

如果去掉了 handle, 则在 catch 块里的 yield 不会被继续执行.

业务场景中使用到的例子:

直播回放弹幕播放

有一个弹幕列表:

const list = [
{ ms: 1000, msg: 'hello1' },
{ ms: 2000, msg: 'hello2' },
{ ms: 2400, msg: 'hello3' },
{ ms: 2800, msg: 'hello4' },
{ ms: 10000, msg: 'hello' },
]

ms 代表毫秒, msg 代表当到达视频 ms 时需要发送的弹幕, 所以需要写一个自动发射弹幕的方法:

function launch(msg, ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms, msg)
})
}
co(function* autoLancher(list) {
for (var [i, obj] of list.entries()) {
const ms = i === 0 ? obj.ms : obj.ms - list[i-1].ms
const message = yield launch(obj.msg, ms)
console.log(message)
}
})(list)