依旧是迟到很久的阅读源码系列 非常感谢若川大佬的阅读源码活动,原文:https://juejin.cn/post/6844904088220467213#heading-11

1. 什么是CO

之前完全没有听说过CO是什么,简单的了解到它是利用了ES6的generator和promise特性实现了异步流程控制的一个库。简单的实现流程就是co在内部将generator对象的next()方法自动执行,实现generator函数的运行,并返回最终运行的结果。

2. 阅读前准备

2.1 简单了解generator

虽然generator函数是一个ES6提供的异步解决方案,但由于自己之前从来没有写过,所以需要“复习”下这块内容。

2.1.1 generator的声明

generator函数声明时与普通函数不一样的地方主要有两个点:

  • function 与 函数名中间必须加一个* 号
  • 定义内部状态时使用yield声明 ```javascript function* testGenerator() { yield ‘test’; yield ‘Gelx’;

    return ‘break’; }

let run = testGenerator();

  1. <a name="yhrCu"></a>
  2. ##### 2.1.2 generator的使用
  3. 使用 `generator`函数时,函数并不会马上执行,它会指向内部状态的指针对象。如果要执行时,必须调用遍历器对象的`next`方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
  4. 更详细的generator函数介绍可以参考[Generator函数的语法](https://es6.ruanyifeng.com/#docs/generator)
  5. <a name="lgcYM"></a>
  6. ### 3. 开始阅读
  7. <a name="kHCEv"></a>
  8. #### 3.1 阅读前疑问
  9. 在简单的了解了co的作用以及`generator`的使用后,有以下几个疑问:
  10. - 怎么样实现依次调用next()方法
  11. - 如何将**yield**后边运算异步结果返回给对应的变量
  12. - co自身怎么返回generator函数最后的return值
  13. <a name="Nvf6H"></a>
  14. #### 3.2 开始阅读
  15. <a name="BZxs5"></a>
  16. ##### 3.2.1 源码地址
  17. co的源码地址为:[https://github.com/tj/co](https://github.com/tj/co)
  18. <a name="vJwEn"></a>
  19. #### 3.2.2 如何使用
  20. 在项目的`Readme.md`中可以找到co的使用方法
  21. ```javascript
  22. co(function* () {
  23. var result = yield Promise.resolve(true);
  24. return result;
  25. }).then(function (value) {
  26. console.log(value);
  27. }, function (err) {
  28. console.error(err.stack);
  29. });

用法也比较简单,调用co函数,里面接收了一个generator函数,同时这个函数中返回了一个Promise。

3.2.3 源码阅读

  1. function co(gen) {
  2. var ctx = this;
  3. // 剔除gen对象,保留其他的入参
  4. var args = slice.call(arguments, 1);
  5. // 返回一个最外围的Promise
  6. return new Promise(function(resolve, reject) {
  7. // 判断gen是不是函数,以及判断gen.next是不是函数,不是函数则直接reslove
  8. if (typeof gen === 'function') gen = gen.apply(ctx, args);
  9. if (!gen || typeof gen.next !== 'function') return resolve(gen);
  10. // 执行第一次next
  11. onFulfilled();
  12. // 先执行gen.next,获取参数
  13. function onFulfilled(res) {
  14. var ret;
  15. try {
  16. ret = gen.next(res);
  17. } catch (e) {
  18. return reject(e);
  19. }
  20. // 将执行返回的结果传入next函数
  21. next(ret);
  22. return null;
  23. }
  24. // 对gen.throw
  25. function onRejected(err) {
  26. var ret;
  27. try {
  28. ret = gen.throw(err);
  29. } catch (e) {
  30. return reject(e);
  31. }
  32. next(ret);
  33. }
  34. // co中做的next执行
  35. function next(ret) {
  36. // 如果gen已经执行完毕,则直接返回结果
  37. if (ret.done) return resolve(ret.value);
  38. // 如果没有执行完成,就封装成Promise继续执行
  39. var value = toPromise.call(ctx, ret.value);
  40. // 判断value是否是Promise,如果是就继续执行,不是就Rejected
  41. if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  42. return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
  43. + 'but the following object was passed: "' + String(ret.value) + '"'));
  44. }
  45. });
  46. }

通过对源码的简单阅读,可以总结出co执行的过程如下:

  • 进入最外层promise
  • 判断传入的gen、gen.next是否为函数,是则继续执行
  • 进入onFulfilled,并记录下generator函数执行的第一个yield返回的参数,并将该参数传入next函数
  • 如果传入next函数的done为true,则返回最外层的promise的resolve
  • 如果传入next函数的done为false,则返回value,并判断该value是否可以转为内部promise对象,如果无法转移,则抛出错误,返回最外层promise的reject
  • 如果可以转化为promise对象,则执行内部promise,通过.then(onFulfilled, onRejected)开始执行
  • 继续在onFulfilled、onRejected内部继续调用next函数,执行yield
  • 当所有的yield执行返回完毕后,将最后的return值返回给最外层promise的resolve
  • 结束调用

3.2.4 next函数

在整个co执行的流程中,最重要的就是next函数,而next函数中最重要的一步则是转化为内部Promise对象,这部分的源码如下:

  1. function toPromise(obj) {
  2. // 确保obj有意义
  3. if (!obj) return obj;
  4. // 若是Promise对象,则直接返回
  5. if (isPromise(obj)) return obj;
  6. // 若是generator函数或者generator对象,则递归调用co函数
  7. if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  8. // 若是函数,则转化promise类函数后再返回
  9. if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  10. // 若是数组,转化promise数组后返回
  11. if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  12. // 若是对象,转化promise对象后返回
  13. if (isObject(obj)) return objectToPromise.call(this, obj);
  14. return obj;
  15. }

4. 实现简单co

4.1 实现简单模拟请求

首先先实现通过调用Promise获取参数

  1. function request(time) {
  2. return new Promise(resolve => {
  3. setTimeout(()=> {
  4. resolve({name:"瘦虎"})
  5. },time)
  6. })
  7. }
  8. // 用yield获取请求到的值
  9. function* getData() {
  10. yield request()
  11. }

4.2 实现简单co

整个co核心功能有两点:

  • 参数传递
  • generator.next 自动执行
  1. function co(gen) {
  2. 1. 获取参数
  3. let ctx = this;
  4. const args = Array.propotype.slice.call(arguments, 1);
  5. gen = gen.apply(ctx,args);
  6. return new Promise((resolve,reject) => {
  7. onFulfilled()
  8. function onFulfilled(res) {
  9. let ret = gen.next(res);
  10. next(ret);
  11. }
  12. function next(ret) {
  13. if(ret.done) return resolve(ret.value);
  14. let promise = ret.value
  15. promise && promise.then(onFulfilled);
  16. }
  17. })
  18. }
  19. // 最后效果
  20. co(function* getData() {
  21. let res = yield request();
  22. console.log(result)
  23. })

总结

这次阅读co花了很长的时间,原因在于除了需要了解co的整套执行逻辑外,还需要了解generator的使用方式、特性、执行规律,这样才能更好的理解为什么会有co函数。虽然源码不多,但看懂整套逻辑后,突然通透的感觉还是很舒服的,希望后续的阅读,我可以提升自己的阅读效率,更高效的完成学习。