Koa 生成器函数探寻

栏目: Node.js · 发布时间: 6年前

内容简介:虽然 Koa 要在下一个 major 版本里移除对生成器 generator 的支持, 但是看一看它对生成器的处理还是能够加深我们对生成器的理解的.Koa 源码中和生成器有关的代码就以下几行, 判断这些依赖都是很短小的单文件, 不如全部粘贴过来.

虽然 Koa 要在下一个 major 版本里移除对生成器 generator 的支持, 但是看一看它对生成器的处理还是能够加深我们对生成器的理解的.

Koa 源码中和生成器有关的代码就以下几行, 判断 use 方法添加的函数是否是生成器函数, 是的话, 将它转换成异步函数. 其中调用的两个函数都是由周边库提供的.

if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
复制代码

isGeneratorFunction

这些依赖都是很短小的单文件, 不如全部粘贴过来.

判断函数是否是一个生成器函数.

'use strict';

var toStr = Object.prototype.toString;
var fnToStr = Function.prototype.toString;
var isFnRegex = /^\s*(?:function)?\*/;
// 这个似乎是用来检测当前执行环境有没有引入生成器函数, 还要再看看
var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
var getProto = Object.getPrototypeOf;
var getGeneratorFunc = function () { // eslint-disable-line consistent-return
  // 如果没有 hasToStringTag, 直接返回 false 表示无法生成生成器函数
  if (!hasToStringTag) {
    return false;
  }
  // 否则尝试利用 Function 生成一个生成器函数并返回
  try {
    return Function('return function*() {}')();
  } catch (e) {
  }
};
var generatorFunc = getGeneratorFunc();
// 如果没有返回生成器函数, 返回一个空对象, 这样最后的判定就会失败
// 如果返回了一个生成器函数, 得到生成器函数的原型对象
var GeneratorFunction = generatorFunc ? getProto(generatorFunc) : {};

module.exports = function isGeneratorFunction(fn) {
  // 不是函数的话肯定也不是生成器函数
  if (typeof fn !== 'function') {
    return false;
  }
  // 将这个函数转换成 string, 然后查看函数字面中是否包含 function*, 有则是一个生成器函数
  // 但是这个判断是很不严谨的, 因为它强制要求写法为 function*
  // 而 function *boo 就没有办法识别了
  if (isFnRegex.test(fnToStr.call(fn))) {
    return true;
  }
  // 如果上面的方法不行, 就尝试利用 toString 的方法
  if (!hasToStringTag) {
    var str = toStr.call(fn);
    return str === '[object GeneratorFunction]';
  }
  // 最后的方法, 通过原型对象判别
  return getProto(fn) === GeneratorFunction;
};
复制代码

convert

并不是将生成器函数转换成异步函数, 而是让它能融入到 Koa 2.0 的工作流程中.

'use strict'

const co = require('co')
const compose = require('koa-compose')

module.exports = convert

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  // 真正核心的代码就这三行
  // 返回了一个符合 koa 中间件函数签名要求的函数, 这个函数内部调用了 co
  const converted = function (ctx, next) {
    // co 函数和中间件在执行的时候, 绑定上下文到 ctx, 也就是 koa 的 context
    // mw.call 的时候, 返回了一个迭代器, 然后 co 去执行这个迭代器, 最终返回一个 Promise
    // 到这里我们有必要知道 koa 要求如何写一个生成器
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

// 这里的生成器返回了迭代器, 当在用户的生成器函数中调用 
function * createGenerator (next) {
  return yield next()
}

// 后面两个方法没有用到, 省略
复制代码

Koa 对生成器的写法要求

在 Koa 1.x 版本中, 中间件要求是生成器函数, 写法如下:

function * legacyMiddleWare(next) {
  yield next
}
复制代码

可以看到, createGenerator(next) 返回的迭代器就是这里的 next.

co

co 是一个迭代器的执行器, 返回一个 Promise.

/**
 * slice() reference.
 */

var slice = Array.prototype.slice;

/**
 * Execute the generator function or a generator
 * and return a Promise.
 *
 * @param {Function} fn
 * @return {Promise}
 * @api public
 */

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.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    // 如果传入的是一个生成器, 那么调用这个生成器以得到一个迭代器
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 如果不存在迭代器或者迭代器没有 next, 那么直接返回一个 resolved 状态的 Promise
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    // 执行这个迭代器
    onFulfilled();

    /**
     * 对迭代器进行一次 next 调用
     * 
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        // 利用上次得到的结果对迭代器进行 next 调用, 得到 yield 出的返回值
        ret = gen.next(res);
      } catch (e) {
        // 有错直接返回出一个拒绝态的 Promise
        return reject(e);
      }
      // 如果没出错就通过 next 进行处理
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        // 如果出错的话会调用迭代器的 throw 尝试解决错误
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a Promise.
     *
     * 得到迭代器的下一个值, 并返回一个 Promise
     * 
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      // 如果迭代已执行完成, 返回一个 resolved 状态的 Promise, resolved undefined
      if (ret.done) return resolve(ret.value);
      // 否则将 value 包装成一个 Promise
      var value = toPromise.call(ctx, ret.value);
      // 如果是包装了 truthy 值的 Promise, 那么通过 then 来后处理
      // 这里的 value 实际是 createGenerator 返回的迭代器封装好的 Promise
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // 如果不能封装为 Promise 则抛出错误
      return onRejected(new TypeError('You may only yield a function, Promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

/**
 * Convert a `yield`ed value into a Promise.
 * 
 * 针对 yield 的 value 可能具有的不同情形来封装成 Promise
 *
 * @param {Mixed} obj
 * @return {Promise}
 * @api private
 */

function toPromise(obj) {
  // 如果为 falsy 直接返回
  if (!obj) return obj;
  // 如果是 Promise 直接返回
  if (isPromise(obj)) return obj;
  // 如果是迭代器或者是生成器就用 co 再执行
  // 实际上 koa 走的是这个分支, 它会再用 co 执行这个迭代器, 返回 Promise
  // 迭代器在执行的时候, 就往 koa middleware 的下游走
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  // 如果是一个 function 那么就通过 thunkToPromise 封装, 不展开了
  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;
}

/**
 * Convert a thunk to a Promise.
 * 
 * 其他的辅助方法从略, 只看看这个.
 *
 * @param {Function}
 * @return {Promise}
 * @api private
 */

function thunkToPromise(fn) {
  var ctx = this;
  return new Promise(function (resolve, reject) {
    fn.call(ctx, function (err, res) {
      if (err) return reject(err);
      if (arguments.length > 2) res = slice.call(arguments, 1);
      resolve(res);
    });
  });
}
复制代码

convert 的执行过程

  1. 当 Koa 要运行生成器函数转换成的中间件的时候, 即调用 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); 时, 执行 return co.call(ctx, mw.call(ctx, createGenerator(next))) , 它返回一个 Promise
  2. 其中, 用户提供的生成器 mw 被调用, 同时调用 createGenerator(next) 返回一个迭代器
  3. co 调用自己的 onFulfilled 方法执行用户的迭代器. 用户会写 yield next 这一句, 将控制权交还给 co, co 调用 next 方法. 此时, 由于 ret.valuecreateGenerator(next) 返回的迭代器, 所以 next 方法进入 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 的分支
  4. value 被封装成一个 Promise, 其实内部又用了一次 co 对 return yield next 进行执行
  5. return yield next 被执行, 进入下游 middleware 并最终回溯到当前的 middleware
  6. co 第二次执行 onFulfilled , 然后调用 next 方法, 此时 ret.done 为真, 返回一个解决态的 Promise
  7. 这里就回到了 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); , 继续往上游回溯

到这里, 我们就梳理清楚了 Koa 1.x 时代所采用的生成器函数是如何被 Koa 2.x 所采用的异步函数兼容的.

可能需要画张图来更清楚地展示这个过程.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

社群运营五十讲

社群运营五十讲

陈菜根 / 北京时代华文书局 / 2018-4-1 / 49.80

物以类聚,人以群分,社群营销不只是简单的建群、卖东西,而是建立一种自动运转的,去中心化的生态圈,让相同爱好的人产生关系,迸发出裂变的火花,创造更多的营销机会。本书从基本的社群概念入手,讲解了社群的五大要素,社群活动的运作,社群的变现模式以及如何做一个社群师等内容,最后再从如何打造社群IP入手,详细讲解了社群IP的定义、分类及操作过程。一起来看看 《社群运营五十讲》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码