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);}
这是一个简单的登录的例子, 有两个异步方法: queryUser
和 crypto.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)