深入koa源码(二):核心库原理

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

内容简介:最近读了 koa2 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍本文来自所有系列文章都放在了

最近读了 koa2 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍 koa 的架构设计 和 3 个核心库,最终会 手动实现一个简易的 koa这是系列第 2 篇,关于 3 个核心库的原理

本文来自 《心谭博客·深入koa源码:核心库原理》

所有系列文章都放在了 Github 。欢迎交流和Star ✿✿ ヽ(°▽°)ノ ✿

is-generator-function:判断 generator

koa2 种推荐使用 async 函数,koa1 推荐的是 generator。koa2 为了兼容,在调用 use 添加中间件的时候,会判断是否是 generator。如果是,则用 covert 库转化为 async 函数。

判断是不是 generator 的逻辑写在了 is-generator-function 库中,逻辑非常简单,通过判断 Object.prototype.toString.call 的返回结果即可:

function* say() {}
Object.prototype.toString.call(say); // 输出: [object GeneratorFunction]

delegates:属性代理

delegates 和 koa 一样,这个库都是出自大佬 TJ 之手。它的作用就是属性代理。这个代理库常用的方法有 gettersettermethodaccess

用法

假设准备了一个对象 target ,为了方便访问其上 request 属性的内容,对 request 进行代理:

const delegates = require("delegates");
const target = {
  request: {
    name: "xintan",
    say: function() {
      console.log("Hello");
    }
  }
};

delegates(target, "request")
  .getter("name")
  .setter("name")
  .method("say");

代理后,访问 request 将会更加方便:

console.log(target.name); // xintan
target.name = "xintan!!!";
console.log(target.name); // xintan!!!
target.say(); // Hello

实现

对于 settergetter 方法,是通过调用对象上的 __defineSetter____defineGetter__ 来实现的。下面是单独拿出来的逻辑:

/**
 * @param {Object} proto 被代理对象
 * @param {String} property 被代理对象上的被代理属性
 * @param {String} name
 */
function myDelegates(proto, property, name) {
  proto.__defineGetter__(name, function() {
    return proto[property][name];
  });
  proto.__defineSetter__(name, function(val) {
    return (proto[property][name] = val);
  });
}

myDelegates(target, "request", "name");
console.log(target.name); // xintan
target.name = "xintan!!!";
console.log(target.name); // xintan!!!

刚开始我的想法是更简单一些,就是直接让 proto[name] = proto[property][name] 。但这样做有个缺点无法弥补,就是之后如果 proto[property][name] 改变, proto[name] 获取不了最新的值。

对于 method 方法,实现上是在对象上创建了新属性,属性值是一个函数。这个函数调用的就是代理目标的函数。下面是单独拿出来的逻辑:

/**
 *
 * @param {Object} proto 被代理对象
 * @param {String} property 被代理对象上的被代理属性
 * @param {String} method 函数名
 */
function myDelegates(proto, property, method) {
  proto[method] = function() {
    return proto[property][method].apply(proto[property], arguments);
  };
}

myDelegates(target, "request", "say");
target.say(); // Hello

因为是“代理”,所以这里不能修改上下文环境。 proto[property][method] 的上下文环境是 proto[property] ,需要 apply 重新指定。

koa 中也有对属性的 access 方法代理,这个方法就是 gettersetter 写在一起的语法糖。

koa-compose:洋葱模型

模拟洋葱模型

koa 最让人惊艳的就是大名鼎鼎的“洋葱模型”。以至于之前我在开发 koa 中间件的时候,一直有种 magic 的方法。经常疑惑,这里 await next() ,执行完之后的中间件又会重新回来继续执行未执行的逻辑。

这一段逻辑封装在了核心库 koa-compose 里面。源码也很简单,算上各种注释只有不到 50 行。为了方便说明和理解,我把其中一些意外情况检查的代码去掉:

function compose(middleware) {
  return function(context) {
    return dispatch(0);

    function dispatch(i) {
      let fn = middleware[i];
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

middleware 里面保存的就是开发者自定义的中间件处理逻辑。为了方便说明,我准备了 2 个中间件函数:

const middleware = [
  async (ctx, next) => {
    console.log("a");
    await next();
    console.log("c");
  },

  async (ctx, next) => {
    console.log("b");
  }
];

现在,模拟在 koa 中对 compose 函数的调用,我们希望程序的输出是: a b c (正如使用 koa 那样)。运行以下代码即可:

const fns = compose(middleware);
fns();

ok,目前已经模拟出来了一个不考虑异常情况的洋葱模型了。

为什么会这样?

为什么会有洋葱穿透的的效果呢?回到上述的 compose 函数,闭包写法返回了一个新的函数,其实就是返回内部定义的 dispatch 函数。其中,参数的含义分别是:

  • i: 当前执行到的中间件在所有中间件中的下标
  • context: 上下文环境。所以我们在每个中间件中都可以访问到当前请求的信息。

在上面的测试用例中, fns 其实就是 dispatch(0) 。在 dispatch 函数中,通过参数 i 拿到了当前要运行的中间件 fn

然后,将当前请求的上下文环境(context)和 dispatch 处理的下一个中间件(next),都传递给当前中间件。对应的代码段是:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

那么,在中间件中执行 await next() ,其实就是执行: await dispatch.bind(null, i + 1) 。因此看起来,当前中间件会停止自己的逻辑,先处理下一个中间件的逻辑。

因为每个 dispatch ,都返回新的 Promsise。所以 async 会等到 Promise 状态改变后再回来继续执行自己的逻辑。

async/await 改写

最后,在不考虑 koa 的上下文环境的情况下,用 async/await 的提炼出了 compose 函数:

function compose(middleware) {
  return dispatch(0);

  async function dispatch(i) {
    let fn = middleware[i];
    try {
      await fn(dispatch.bind(null, i + 1));
    } catch (err) {
      return err;
    }
  }
}

下面是它的使用方法:

const middleware = [
  async next => {
    console.log("a");
    await next();
    console.log("c");
  },

  async next => {
    console.log("b");
  }
];

compose(middleware); // 输出a b c

希望最后这段代码能帮助理解!


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

查看所有标签

猜你喜欢:

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

Beginning Google Maps API 3

Beginning Google Maps API 3

Gabriel Svennerberg / Apress / 2010-07-27 / $39.99

This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具