关于前端:50行代码串行Promisekoa洋葱模型原来是这么实现

38次阅读

共计 5844 个字符,预计需要花费 15 分钟才能阅读完成。

1. 前言

大家好,我是若川。欢送关注我的公众号若川视线,最近组织了 源码共读流动《1 个月,200+ 人,一起读了 4 周源码》,感兴趣的能够加我微信 ruochuan12 参加,长期交流学习。

之前写的《学习源码整体架构系列》蕴含 jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4 十余篇源码文章。其中最新的两篇是:

Vue 3.2 公布了,那尤雨溪是怎么公布 Vue.js 的?

初学者也能看懂的 Vue3 源码中那些实用的根底工具函数

写绝对很难的源码,消耗了本人的工夫和精力,也没播种多少浏览点赞,其实是一件挺受打击的事件。从浏览量和读者受害方面来看,不能促成作者继续输入文章。

所以转变思路,写一些绝对通俗易懂的文章。其实源码也不是设想的那么难,至多有很多看得懂

之前写过 koa 源码文章学习 koa 源码的整体架构,浅析 koa 洋葱模型原理和 co 原理比拟长,读者敌人大概率看不完,所以本文从koa-compose50 行源码讲述。

本文波及到的 koa-compose 仓库 文件,整个 index.js 文件代码行数尽管不到 50 行,而且测试用例 test/test.js 文件 300 余行,但十分值得咱们学习。

歌德曾说:读一本好书,就是在和崇高的人谈话。同理可得:读源码,也算是和作者的一种学习交换的形式。

浏览本文,你将学到:

1. 相熟 koa-compose 中间件源码、能够应答面试官相干问题
2. 学会应用测试用例调试源码
3. 学会 jest 局部用法

2. 环境筹备

2.1 克隆 koa-compose 我的项目

本文仓库地址 koa-compose-analysis,求个star~

# 能够间接克隆我的仓库,我的仓库保留的 compose 仓库的 git 记录
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i

顺带说下:我是怎么保留 compose 仓库的 git 记录的。

# 在 github 上新建一个仓库 `koa-compose-analysis` 克隆下来
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose-analysis
git subtree add --prefix=compose https://github.com/koajs/compose.git main
# 这样就把 compose 文件夹克隆到本人的 git 仓库了。且保留的 git 记录

对于更多 git subtree,能够看这篇文章用 Git Subtree 在多个 Git 我的项目间双向同步子项目,附扼要使用手册

接着咱们来看怎么依据开源我的项目中提供的测试用例调试源码。

2.2 依据测试用例调试 compose 源码

VSCode(我的版本是 1.60)关上我的项目,找到 compose/package.json,找到 scriptstest 命令。

// compose/package.json
{
    "name": "koa-compose",
    // debug(调试)"scripts": {
        "eslint": "standard --fix .",
        "test": "jest"
    },
}

scripts 上方应该会有 debug 或者 调试 字样。点击debug(调试),抉择 test

接着会执行测试用例 test/test.js 文件。终端输入如下图所示。

接着咱们调试 compose/test/test.js 文件。
咱们能够在 45 行 打上断点,从新点击 package.json => srcipts => test 进入调试模式。
如下图所示。

接着按上方的按钮,持续调试。在 compose/index.js 文件中要害的中央打上断点,调试学习源码事倍功半。

更多 nodejs 调试相干 能够查看官网文档

顺便提一下几个调试相干按钮。

    1. 持续(F5)
    1. 单步跳过(F10)
    1. 单步调试(F11)
    1. 单步跳出(Shift + F11)
    1. 重启(Ctrl + Shift + F5)
    1. 断开链接(Shift + F5)

接下来,咱们跟着测试用例学源码。

3. 跟着测试用例学源码

分享一个测试用例小技巧:咱们能够在测试用例处加上 only 润饰。

// 例如
it.only('should work', async () => {})

这样咱们就能够只执行以后的测试用例,不关怀其余的,不会烦扰调试。

3.1 失常流程

关上 compose/test/test.js 文件,看第一个测试用例。

// compose/test/test.js
'use strict'

/* eslint-env jest */

const compose = require('..')
const assert = require('assert')

function wait (ms) {return new Promise((resolve) => setTimeout(resolve, ms || 1))
}
// 分组
describe('Koa Compose', function () {it.only('should work', async () => {const arr = []
    const stack = []

    stack.push(async (context, next) => {arr.push(1)
      await wait(1)
      await next()
      await wait(1)
      arr.push(6)
    })

    stack.push(async (context, next) => {arr.push(2)
      await wait(1)
      await next()
      await wait(1)
      arr.push(5)
    })

    stack.push(async (context, next) => {arr.push(3)
      await wait(1)
      await next()
      await wait(1)
      arr.push(4)
    })

    await compose(stack)({})
    // 最初输入数组是 [1,2,3,4,5,6]
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })
}

大略看完这段测试用例,context是什么,next又是什么。

koa 的文档上有个十分代表性的中间件 gif 图。

compose 函数作用就是把增加进中间件数组的函数依照下面 gif 图的程序执行。

3.1.1 compose 函数

简略来说,compose 函数次要做了两件事件。

    1. 接管一个参数,校验参数是数组,且校验数组中的每一项是函数。
    1. 返回一个函数,这个函数接管两个参数,别离是 contextnext,这个函数最初返回Promise
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
function compose (middleware) {
  // 校验传入的参数是数组,校验数组中每一项是函数
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch(i){// 省略,下文讲述}
  }
}

接着咱们来看 dispatch 函数。

3.1.2 dispatch 函数

function dispatch (i) {
  // 一个函数中屡次调用报错
  // await next()
  // await next()
  if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  index = i
  // 取出数组里的 fn1, fn2, fn3...
  let fn = middleware[i]
  // 最初 相等,next 为 undefined
  if (i === middleware.length) fn = next
  // 间接返回 Promise.resolve()
  if (!fn) return Promise.resolve()
  try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
  } catch (err) {return Promise.reject(err)
  }
}

值得一提的是:bind函数是返回一个新的函数。第一个参数是函数里的 this 指向(如果函数不须要应用 this,个别会写成null)。
这句 fn(context, dispatch.bind(null, i + 1)i + 1 是为了 let fn = middleware[i]middleware 中的下一个函数。
也就是 next 是下一个中间件里的函数。也就能解释上文中的 gif图函数执行程序。
测试用例中数组的最终程序是[1,2,3,4,5,6]

3.1.3 简化 compose 便于了解

本人入手调试之后,你会发现 compose 执行后就是相似这样的构造(省略 try catch 判断)。

// 这样就可能更好了解了。// simpleKoaCompose
const [fn1, fn2, fn3] = stack;
const fnMiddleware = function(context){
    return Promise.resolve(fn1(context, function next(){
        return Promise.resolve(fn2(context, function next(){
              return Promise.resolve(fn3(context, function next(){return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};

也就是说 koa-compose 返回的是一个 Promise,从 中间件(传入的数组)中取出第一个函数,传入 context 和第一个 next 函数来执行。

第一个 next 函数里也是返回的是一个 Promise,从 中间件(传入的数组)中取出第二个函数,传入 context 和第二个 next 函数来执行。

第二个 next 函数里也是返回的是一个 Promise,从 中间件(传入的数组)中取出第三个函数,传入 context 和第三个 next 函数来执行。

第三个 …

以此类推。最初一个中间件中有调用 next 函数,则返回 Promise.resolve。如果没有,则不执行next 函数。
这样就把所有中间件串联起来了。这也就是咱们常说的洋葱模型。

不得不说十分惊艳,“玩还是大神会玩”

3.2 谬误捕捉

it('should catch downstream errors', async () => {const arr = []
  const stack = []

  stack.push(async (ctx, next) => {arr.push(1)
    try {arr.push(6)
      await next()
      arr.push(7)
    } catch (err) {arr.push(2)
    }
    arr.push(3)
  })

  stack.push(async (ctx, next) => {arr.push(4)
    throw new Error()})

  await compose(stack)({})
  // 输入程序 是 [1, 6, 4, 2, 3]
  expect(arr).toEqual([1, 6, 4, 2, 3])
})

置信了解了第一个测试用例和 compose 函数,也是比拟好了解这个测试用例了。这一部分其实就是对应的代码在这里。

try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {return Promise.reject(err)
}

3.3 next 函数不能调用屡次

it('should throw if next() is called multiple times', () => {
  return compose([async (ctx, next) => {await next()
      await next()}
  ])({}).then(() => {throw new Error('boom')
  }, (err) => {assert(/multiple times/.test(err.message))
  })
})

这一块对应的则是:

index = -1
dispatch(0)
function dispatch (i) {if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  index = i
}

调用两次后 iindex 都为 1,所以会报错。

compose/test/test.js文件中总共 300 余行,还有很多测试用例能够依照文中办法自行调试。

4. 总结

尽管 koa-compose 源码 50 行 不到,但如果是第一次看源码调试源码,还是会有难度的。其中混杂着高阶函数、闭包、Promisebind等基础知识。

通过本文,咱们相熟了 koa-compose 中间件常说的洋葱模型,学会了局部 jest 用法,同时也学会了如何应用现成的测试用例去调试源码。

置信学会了通过测试用例调试源码后,会感觉源码也没有设想中的那么难

开源我的项目,个别都会有很全面的测试用例。除了能够给咱们学习源码调试源码带来不便的同时,也能够给咱们带来的启发:本人工作中的我的项目,也能够逐渐引入测试工具,比方 jest

此外,读开源我的项目源码是咱们学习业界大牛设计思维和源码实现等比拟好的形式。

看完本文,十分心愿能本人入手实际调试源码去学习,容易排汇消化。另外,如果你有余力,能够持续看我的 koa-compose 源码文章:学习 koa 源码的整体架构,浅析 koa 洋葱模型原理和 co 原理

正文完
 0