🙇‍♂️

co函数的理解

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)

在我们一生中,命运赐予我们每个人三个导师,三个朋友,三名敌人,三个挚爱。但这十二人总是不以真面目示人,总要等到我们爱上他们、离开他们、或与他们对抗时,才能知道他们是其中哪种角色。