Abstract

本系列是关于Koa框架的文章,目前关注版本是Koa v1。主要分为以下几个方面:

  1. Koa源码分析(一) -- generator
  2. Koa源码分析(二) -- co的实现
  3. Koa源码分析(三) -- middleware机制的实现

co

大名鼎鼎的co是什么?它是TJ大神基于ES6的一些新特性开发的异步流程控制库,基于它所开发的koa被视为未来主流的web框架。

koa基于co实现,而co又是使用了ES6的generator和promise特性。如果还不理解,可以查看阮一峰老师的《ECMAScript 6 入门 --- Generator》《ECMAScript 6 入门 --- Promise》。目前co升级为4.X版本事,代码进行了一次颇有规模的重构,我们主要关注co(4.X)的实现思路和源码分析。

使用示例

  1. co(function* (){
  2. var a = yield Promise.resolve('one');
  3. console.log(a);
  4. var b = yield Promise.reslove('two');
  5. console.log(b);
  6. return 'three';
  7. }).then((value) => console.log(value));
  8. // one
  9. // two
  10. // three
  1. co(function* (){
  2. var res = yield [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
  3. return res;
  4. }).then((value) => console.log(res));
  5. // [1, 2, 3]

根据co的功能,它作为异步流程控制的作用,自动调用generator对象的next()方法,实现generator函数的运行,并返回最终运行的结果。

如果要涉及到co的实现细节,我们就会存在以下几个疑问:

  1. 如何依次调用next()方法
  2. 如何将yield后边运算异步结果返回给对应的变量
  3. co自身如何返回generator函数最后的return值

接下来我们正对以上问题,分析TJ大神的源码

源码解析

co源码的流程控制

  1. function co(gen) {
  2. // 保持当前函数的上下文
  3. var ctx = this;
  4. // 截取co输入的参数,剔除arguments中的第一个参数,即gen对象,剩余参数作为gen的入参
  5. var args = slice.call(arguments, 1);
  6. // 返回一个Promise对象,即最外围Promise对象
  7. return new Promise(function(resolve, reject) {
  8. // 判断传入的gen是否为函数,若是则执行,将结果赋值给gen对象
  9. // 若不是,则不执行
  10. if (typeof gen === 'function') gen = gen.apply(ctx, args);
  11. // 根据generator函数执行结果是否存在next字段,判断gen是否为generator迭代器对象
  12. // 若不是,则调用resolve返回最外围Promise对象的状态
  13. if (!gen || typeof gen.next !== 'function') return resolve(gen);
  14. // 若是generator迭代器对象,开始控制gen.next()方法的调用
  15. onFulfilled();
  16. // 两个用途
  17. // 一、generator函数的执行入口
  18. // 二、当做所有内部Promise对象的resolve方法,处理异步结果,并继续调用下一个Promise
  19. function onFulfilled(res) {
  20. var ret;
  21. try {
  22. // gen运行至yield处被挂起,开始处理异步操作,并将异步操作的结果返回给ret.value
  23. ret = gen.next(res);
  24. } catch (e) {
  25. // 若报错,直接调用reject返回外围Promise对象的状态,并传出错误对象
  26. return reject(e);
  27. }
  28. // 将gen.next的执行结果传入next函数,实现依次串行调用gen.next方法
  29. next(ret);
  30. return null;
  31. }
  32. // 当做所有内部Promise对象的reject方法,处理异步结果,并继续调用下一个Promise
  33. function onRejected(err) {
  34. var ret;
  35. try {
  36. ret = gen.throw(err);
  37. } catch (e) {
  38. // 若报错,直接调用reject返回外围Promise对象的状态,并传出错误对象
  39. return reject(e);
  40. }
  41. // 将gen.throw的执行结果传入next函数,实现依次串行调用gen.next方法
  42. next(ret);
  43. }
  44. // 实现串行调用gen.next的核心
  45. function next(ret) {
  46. // 判断内部Promise是否全部执行完毕
  47. // 若执行完毕,直接调用resolve改变外围Promise的状态,并返回最终的return值[问题3]
  48. if (ret.done) return resolve(ret.value);
  49. // 若未执行完毕,调用toPromise方法将上一个Promise返回的值转化为Promise对象
  50. // 具体参见toPromise方法
  51. var value = toPromise.call(ctx, ret.value);
  52. // 根据value转化后的Promise对象的两个状态,执行下一个next方法
  53. if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  54. // 抛出不符合转化规则的类型的值
  55. return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
  56. + 'but the following object was passed: "' + String(ret.value) + '"'));
  57. }
  58. });
  59. }

源码分析完了,我们可以把co串行调用generator函数中yield的过程总结如下:

  1. 进入外围Promise
  2. 通过入口onFilfilled()方法,将generator函数运行至第一个yield处,执行该yield后边的异步操作,并将结果传入next方法
  3. 如果next中传入结果的done为true,则返回外围Promise的resolve
  4. 如果next中传入结果的done为true,则返回value(即yield后边的对象)是否可以转化为内部Promise对象。如无法转化,则抛出错误,返回外围Promise的reject
  5. 若能转化为Promise对象,将所有内部Promise并行执行,通过then(onFilfilled, onRejected)开始执行
  6. 在onFilfilled()或者onRejected()内部调用再次调用next()方法,实现串行执行yield,并肩yield后边的对象传递给next(),依次重复。
  7. 所有yield执行返回,将最后的return值返回给外围Promise的resovle方法,结束co对generator函数的调用

yield后面对象转化为Promise

能够在co中实现generator函数的逐步调用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,并返回新co的外围Promise
  7. // 作为当前co的内部Promise,这样实现多层级调用
  8. if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  9. // 若是函数,则返回thunk规范的函数
  10. if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  11. // 若是数组,把数组中每个元素转化为内部Promise,返回Promise.all并行运算
  12. if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  13. // 若是对象,遍历对象中的每个key对应的value,转化成Promise.all并行运算
  14. if (isObject(obj)) return objectToPromise.call(this, obj);
  15. return obj;
  16. }
  17. function thunkToPromise(fn) {
  18. var ctx = this;
  19. return new Promise(function (resolve, reject) {
  20. fn.call(ctx, function (err, res) {
  21. if (err) return reject(err);
  22. if (arguments.length > 2) res = slice.call(arguments, 1);
  23. resolve(res);
  24. });
  25. });
  26. }
  27. function arrayToPromise(obj) {
  28. // Array.map并行计算返回每一个元素的Promise
  29. return Promise.all(obj.map(toPromise, this));
  30. }
  31. function objectToPromise(obj){
  32. var results = new obj.constructor();
  33. var keys = Object.keys(obj);
  34. var promises = [];
  35. for (var i = 0; i < keys.length; i++) {
  36. var key = keys[i];
  37. var promise = toPromise.call(this, obj[key]);
  38. if (promise && isPromise(promise)) defer(promise, key);
  39. else results[key] = obj[key];
  40. }
  41. // Promise链式调用,后续的then能偶获取此处的results
  42. return Promise.all(promises).then(function () {
  43. return results;
  44. });
  45. function defer(promise, key) {
  46. // key对应的元素成功转化为Promise对象后,构造这些Promise的resovle方法
  47. // 以便在results中获取每个Promise对象成功执行后结果
  48. results[key] = undefined;
  49. promises.push(promise.then(function (res) {
  50. results[key] = res;
  51. }));
  52. }
  53. }

结合上述分析,我们可以得到,yield后面只能是函数、Promise对象、Generator函数、Generator迭代器对象、数组(元素仅限之前的4类)和Object(对应value仅限定之前的4类)

Koa源码分析(二) -- co的实现的更多相关文章

  1. Koa源码分析(三) -- middleware机制的实现

    Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...

  2. Koa源码分析(一) -- generator

    Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: 1. Koa源码分析(一) -- generator 2. Koa源码分析(二) -- co的实现 ...

  3. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  4. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

  5. Tomcat源码分析二:先看看Tomcat的整体架构

    Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...

  6. 十、Spring之BeanFactory源码分析(二)

    Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...

  7. Vue源码分析(二) : Vue实例挂载

    Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...

  8. 多线程之美8一 AbstractQueuedSynchronizer源码分析<二>

    目录 AQS的源码分析 该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制. 1.条件队列 条件队列与AQS中的同步队列有所不同,结构图如下: 两者区别: 1.链表 ...

  9. Node.js躬行记(19)——KOA源码分析(上)

    本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...

随机推荐

  1. 容器部署解决方案Docker

      容器部署解决方案Docker 课程目标 目标1:了解Docker与虚拟机的不同点,相比的优势 目标2:掌握Docker的启动方法 目标3:掌握Docker镜像操作 目标4:掌握Docker容器操作 ...

  2. Split CSV/TXT file

    void Main(){ var path = @"c:\sourceGit\speciesLatLon.txt"; var inputLines = File.ReadAllLi ...

  3. (ZT)算法杂货铺——分类算法之决策树(Decision tree)

    https://www.cnblogs.com/leoo2sk/archive/2010/09/19/decision-tree.html 3.1.摘要 在前面两篇文章中,分别介绍和讨论了朴素贝叶斯分 ...

  4. Cordova开发App使用USB进行真机调试

    在使用cordova开发app时,不像浏览器中可以直接使用浏览器的开发者工具进行调试.为了看到app的显示效果, 一种是使用模拟器进行展示,一种是使用真机进行展示. 模拟器:可以使用Android s ...

  5. webstorm没有及时将改动保存到文件盘的问题

    webpack经常监听不到webstorm的改动,即使手动ctrl+s了,导致无法触发编译,去google查了下,发现webstorm有一个“save write”的功能,见下图: 这选项的作用应该是 ...

  6. generator 生成器

    L=[i*i for i in range(10)] print(L) G=(i*i for i in range(10)) #变中括号为小括号 print(G) 另一种方法: fib(max): n ...

  7. Chen qiaoqiao Studio

    Welcome here! If you need any help, please contact us. Contact info Email: lovey_kids@163.com

  8. letecode242有效字母的异位词

    bool isAnagram(char* s, char* t) { ] = {}; ] = {}; int lenS = strlen(s); int lenT = strlen(t); ;i< ...

  9. Taro开发写密码支付弹层

    在支付的时候弹出填写密码,模仿了支付宝支付填写密码.主要是利用遮罩的来实现.直接上代码吧. html设计,通过标记控制显示. { showPayPwdInput ? <View classNam ...

  10. GitLab管理之 - Gitlab 用户管理

    1. 移除用户 (1) 使用管理员登陆Gitlab服务器 (2) 点击管理区域 (3) 点击Users. (4)点击[Block User] 2. 添加用户(1)用root 管理员登陆.(2)点击[管 ...