资讯专栏INFORMATION COLUMN

co模块用法及分析

muzhuyu / 2597人阅读

摘要:模块可以将异步解放成同步。源码分析使用的模块版本号为首先看一些用于判断对象类型的函数对数组方法的引用这两个应该就不用说了吧。。。看一下模块的输出部分因此以下三种用法等价接着就是重头戏函数了。

本文只在个人博客和 SegmentFault 社区个人专栏发表,转载请注明出处
个人博客: https://zengxiaotao.github.io
SegmentFault 个人专栏: https://segmentfault.com/blog...

写在前面

学 nodejs 当然避免不了学习框架,毕竟原生的 API 较底层。最先接触的是 Koa 。看看官网的描述

next generation web framework for node.js

我翻译一下就是: 基于 node.js 的下一代 web 开发框架。好像很厉害的样子!koa 是一个轻量级的框架,本质上提供了一个架子,通过 各种中间件的级联的方式实现特定的功能。koa 借助 promise 和 generator , 很好解决了异步组合问题。

那什么又是 co 。学习 koa 就一定少不了学习 co 模块。co 模块可以将异步解放成同步。co 函数接受一个 generator 函数作为参数,在函数内部自动执行 yield 。

co 源码分析

使用的 co 模块版本号为 4.6.0

首先看一些用于判断对象类型的函数

var slice = Array.prototype.slice; // 对数组 slice 方法的引用 
function isObject(val) {
  return Object == val.constructor;
}

这两个应该就不用说了吧。。。

function isPromise(obj) {
  return "function" == typeof obj.then;
}

判断一个对象是否是一个 promise 实例,判断的依据也很简单,根据 “鸭子类型”,判断这个对象是否有 then 方法

function isGenerator(obj) {
  return "function" == typeof obj.next && "function" == typeof obj.throw;
}

类似的,判断一个对象时候是 generator 实例,只需判断这个对象是否具有 next 方法和 throw 方法。

function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ("GeneratorFunction" === constructor.name || "GeneratorFunction" === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}

判断是否是一个 generator 函数,只需判断这个函数是否是 GeneratorFunction 函数的实例

以上所讲的在之后将 value 包装成 promise 实例时都会用到。

看一下 co 模块的输出部分

module.exports = co["default"] = co.co = co

因此以下三种用法等价

var co = require("co") // (1)
var co = require("co").co // (2)
var co = require("co").default // (3)

接着就是重头戏 co 函数了。

function co(gen) {
  var ctx = this; // 保存函数的执行上下文对象
  var args = slice.call(arguments, 1) // 传给 gen 函数的参数
  // 返回一个 promise 实例
  return new Promise(function(resolve, reject) {
       // 根据传入的 generator 函数生成一个 generator 实例
    if (typeof gen === "function") gen = gen.apply(ctx, args);
    // 如果生成的 gen 不是一个 generator 实例, 
    // promise 直接变成 resolved 状态
    if (!gen || typeof gen.next !== "function") return resolve(gen);
     // 执行 onFulfilled 方法
    onFulfilled();

    function onFulfilled(res) {
      var ret;
      try {
        // 执行 gen 的 next 方法
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      // 并将这个值传入 next 函数
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      // 如果 gen 执行完毕, ret.done 变为 true ,那么这个 promise 的实例
      // 的状态自然变成了 resolved
      if (ret.done) return resolve(ret.value); 
      var value = toPromise.call(ctx, ret.value); // 将 value 重新包装成一个 promise 实例
      // 新返回的 promise 实例的 resolve 方法设置为 onFulfilled 函数,再次执行 next 方法, 从而实现了自动调用 generator 实例的 next 方法
      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) + """));
    }
  });
}

以上,就是 co 模块就实现了自动执行 generator 实例的 next 方法。那么接下来看看 co 是怎么把一个值转化为一个 promise 实例。

function toPromise(obj) {
  if (!obj) return obj;  // 如果传入的 obj 是假值,返回这个假值 如 undefined , false, null
  if (isPromise(obj)) return obj; // 如果是 Promise 实例,返回这个 promise 实例
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); // 如果是 generator 函数或者 一个generator
  if ("function" == typeof obj) return thunkToPromise.call(this, obj); // 如果是 thunk 函数
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj); // 如果是一个数组
  if (isObject(obj)) return objectToPromise.call(this, obj); // 如果是一个 plain object
  return obj; // 如果是原始值,则返回这个原始值。
}

那么每个函数依次看下去。

function thunkToPromise(fn) {
  var ctx = this; // 保存函数上下文对象
  // 返回一个 promise 实例
  return new Promise(function (resolve, reject) {
    // 执行传入的 thunk 函数
    // thunk 函数接受一个 回调函数 作为参数
    fn.call(ctx, function (err, res) {
        // 如果 thunk 函数运行错误
        // promise 实例的 变为 rejected 状态,执行 reject 函数,也就是 co 函数内定义的 onRejected 函数,下同
      if (err) return reject(err);
          // 获得多余参数
      if (arguments.length > 2) res = slice.call(arguments, 1);
      // promise 状态变为 resolved ,执行 resolve 函数,也就是 onFulfilled 函数
      resolve(res);
    });
  });
}

所以,总结一下就是说,如果 generator 里 yield 后面是一个 thunk 函数, 这个 thunk 函数接受一个回调参数作为参数,co 在这个回调函数里定义了何时将 promise 的状态变为 resolved 或者 rejected ,

function arrayToPromise(obj) {
  // Promise.all 方法返回一个 新的 promise 实例
  // 如果 obj 是一个数组,把每个元素包装成一个 promise 实例
  // 如果每一个 promise 如果都变为 resolved 状态
  // 那么返回的新的 promise 实例的状态变为 resloved 状态
  // 传给 resolve 函数的参数为之前每个 promise 的返回值所组成的数组 
  return Promise.all(obj.map(toPromise, this)); 
}

同样,如果 obj 是一个数组,也就是 yield 语句后面的表达式的值为一个数组,那么就执行 Promise.all 方法, 将数组的每一项都变成一个 promise 实例。

具体方法如下:

使用 toPromise 方法将 obj 数组中的每一项都包装成一个 promise 实例

如果上一步中的数组中有元素不是 promise 实例,Promise.all 方法将调用 Promise.resolve 方法,将其转化为 promise 实例。

Promise.all 方法返回一个新的 promise 实例。

只有 promise 实例数组中的所有实例的状态都变为 resolved 状态时,这个新的 promise 实例的状态才会变成 resolved。只要数组中有一个 promise 实例的状态变为 rejected ,新的promise 实例状态也马上变为 rejected 。

当返回的新的 promise 实例状态变为 resolved 时,传入其 resolve 函数的参数为之前数组中每个 promise 实例调用 resolve 函数的返回值组成的数组。如果返回的新的 promise 的状态变为 rejected ,那么传给 reject 函数的参数为数组中的 promise 实例最先变为 rejected 状态的那一个执行 reject 函数的返回值。

真绕口,多看几遍应该就能理解了。

最后来看看如果 ret.value 如果是一个对象,co 模块是怎么样把它变成一个 promise 实例的。

function objectToPromise(obj){
  // 定义一个空对象
  var results = new obj.constructor();
  // 获取 obj 的全部属性
  var keys = Object.keys(obj);
  // 用于盛放 每个属性值生成的对应的 promise 实例
  var promises = []; 
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]); // 根据属性值生成一个 promise 实例
    if (promise && isPromise(promise)) defer(promise, key); 
    else results[key] = obj[key];
  }
  // 通过一个 promise.all 方法返回一个新的实例
  return Promise.all(promises).then(function () {
    return results; // 将 results 作为 onFulfilled 函数的参数
  });
  // 函数的作用
  // 给 promise 添加 resolve 函数
  // 并且把这个 promise 实例推入 promises 数组
  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;

    promises.push(promise.then(function (res) {
      results[key] = res; // 定义promise 实例的 resolve 函数
    }));
  }
}
总结

分析完 co 的整个源码总结一下整个执行的过程。首先,co 函数接受一个 generator 函数,并且在 co 函数内部执行,生成一个 generator 实例。调用 generator 的 next 方法, 对生成的对象的 value 属性值使用 toPromise 方法,生成一个 promise 实例,当这个 promise 实例的状态变为 resolved 时,执行 onFulfilled 方法,再次对 generator 实例执行 next 方法,然后重复整个过程。如果出现错误,则执行这个 promise 实例定义的 reject 函数即 onRejected 方法。

以上即实现了将异步过程同步化。

最后欢迎 star

https://github.com/zengxiaotao/zengxiaotao.github.io

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91339.html

相关文章

  • co源码分析其实践

    摘要:返回的结果是一个对象,类似于表示本次后面执行之后返回的结果。对象用于一个异步操作的最终完成或失败及其结果值的表示简单点说就是处理异步请求。源码分析主要脉络函数调用后,返回一个实例。参考链接解释对象的用法的源码及其用法 本文始发于我的个人博客,如需转载请注明出处。为了更好的阅读体验,可以直接进去我的个人博客看。 前言 知识储备 阅读本文需要对Generator和Promise有一个基本的...

    vincent_xyb 评论0 收藏0
  • 前端每周清单第 10 期:Firefox53、React VR发布、Microsoft Edge现代

    摘要:新闻热点国内国外,前端最新动态发布近日,正式发布新版本中提供了一系列的特性与问题修复。而近日正式发布,其能够帮助开发者快速构建应用。 前端每周清单第 10 期:Firefox53、React VR发布、JS测试技术概述、Microsoft Edge现代DOM树构建及性能之道 为InfoQ中文站特供稿件,首发地址为这里;如需转载,请与InfoQ中文站联系。从属于笔者的 Web 前端入门...

    MingjunYang 评论0 收藏0
  • 前端每周清单半年盘点之 Node.js 篇

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。对该漏洞的综合评级为高危。目前,相关利用方式已经在互联网上公开,近期出现攻击尝试爆发的可能。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢...

    kid143 评论0 收藏0
  • 前端每周清单半年盘点之 CSS 篇

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。它能够为我们提供类似于预处理器命名空间等多方面的辅助。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢迎关注【前端之巅】微信公众号(ID:f...

    RaoMeng 评论0 收藏0
  • 常用npm模块分享

    摘要:平时自己用的模块也不算少了,其实网上有很多牛人开发的模块都很好,希望不要被埋没了。一实用的模块作用获取最新可用的迅雷账号。用法截图查看用户某个时间段内所有模块的下载量,按从高到低排名。 平时自己用的npm模块也不算少了,其实网上有很多牛人开发的npm模块都很好,希望不要被埋没了。showImg(http://static.xiaomo.info/images/npm.png); 一、 ...

    sorra 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<