co函数库 源码解析

9/17/2021 ES6

# 前言

co函数库是 TJ Holowaychuk (opens new window)大神的作用,解决的问题就是 Generator 函数的自动执行。

# Generator函数所带来的问题

Generator函数是 es6规范提出用来解决异步编程的一种方式,我们通过一个例子,来看一下,它是如何解决的。

function asyncFn(msg) {
  setTimeout(() => {
    console.log(msg)
    t.next()
  }, 2000);
}
function* test() {
  console.log('start')
  yield asyncFn('hello')
  yield asyncFn('world')
  console.log('end')
}
const t = test()
t.next()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里我们需要调用 t.next() 来开启整个函数的执行,如果内部有多个yield需要我们手动调用的话,需要写多个t.next()co函数优化了 Generator

function asyncFn (msg) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(msg)
      resolve()
    }, 2000);
  })
}
function* test() {
  console.log('start')
  yield asyncFn('hello')
  yield asyncFn('world')
  console.log('end')
}
co(test).then(() => {
  console.log('test')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样的调用方式比上述原始的调用方式要优雅很多,而却避免了 t.next() 在异步函数内部调用的问题。

# co函数实现原理

基于 Promise 的自动调用执行,在每一个 yield 的返回必须是一个 Promise 对象,然后判断 t.next() 返回值中 done的值,决定是否继续执行,这样就可以完成generator函数的自执行了。下面是简单实现:

function run(genFn) {
  const t = genFn()
  function next(data) {
    const res = t.next(data)
    if (!res.done) {
      res.value.then((val) => {
        next(val)
      })
    } else {
      return res.value
    }
  }
  next()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们来测试一下:

// demo
function asyncFn (msg) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(msg)
      resolve('world')
    }, 2000);
  })
}
function* test() {
  console.log('start')
  const res = yield asyncFn('hello')
  yield asyncFn(res)
  console.log('end')
}
run(test)
// 打印结果
// start
// hello
// world
// end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# co函数源码解析

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);
  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // 我们将所有东西都包装在一个 promise 中以避免 promise 链接,这会导致内存泄漏错误
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
  })
}
1
2
3
4
5
6
7
8
9
10

co 函数声明第一层就是用 Promise 进行包裹,这里有两个作用:

  1. 一个是避免promise链式调用而导致内存溢出
  2. 在后续的回调之中可以使用 .then 继续后面逻辑的书写 除此之外我们还需要 resolvereject 函数的执行域
return new Promise(function(resolve, reject) {
  if (typeof gen === 'function') gen = gen.apply(ctx, args) // 兼容 gen 方式传入,也可以是 gen()
  if (!gen || typeof gen.next !== 'function') return resolve(gen)

  onFulfilled(); // 开启调用
  function onFulfilled(res) {
    var ret;
    try {
      ret = gen.next(res);
    } catch (e) {
      return reject(e);
    }
    next(ret);
    return null;
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这里主要是调用了 onFulfilled 来开启调用,使用 try-catch 捕获 gen.next() 异常,然后继续调用 next, 重点关注 next 里面的逻辑。

function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
    + 'but the following object was passed: "' + String(ret.value) + '"'));
}
1
2
3
4
5
6
7

这个 next 函数的实现大体功能上和我们上述的简易实现差不多。如果generator内部执行已完成,即ret.donetrue,直接resolve();否则使用toPromise包裹ret.value使其promise化,value.then(onFulfilled, onRejected),继续上述的循环。

上述的代码已经很好的解决了 generator 函数的自执行的问题了,除此之外,还有一些不错的工具函数,我们来看看:

比如 toPromise 这个函数,为了保证后续可以使用 promise.then 来保证异步的顺序调用。

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}
1
2
3
4
5
6
7
8
9
  1. ret.value 返回值必须是 function, promise, generator, array, or object中的一种,不然就报错了。

Todo: https://github.com/tj/co/issues/180

上次更新: 2/15/2025, 2:29:28 PM