最近将内部测试框架的底层库从 mocha 迁移到了 AVA, 迁移的原因之一是因为 AVA 提供了更好的流程控制
我们从一个例子开始入手:
有 A,B,C,D4 个 case, 我要实现
A -->> B -->> (C | D)
,A 最先执行, B 等待 A 执行完再执行, 最后是 (C | D) 并发执行, 使用 ava 提供的 API 来完成 case 就是:
- const ava = require('ava')
- ava.serial('A', async () => {
- // do something
- })
- ava.serial('B', async () => {
- // do something
- })
- ava('C', async () => {
- // do something
- })
- ava('D', async () => {
- // do something
- })
接下来我们就来具体看下 AVA 内部是如何实现流程控制的:
在 AVA 内实现了一个 Sequence 类:
- class Sequence {
- constructor (runnables) {
- this.runnables = runnables
- }
- run() {
- // do something
- }
- }
这个 Sequence 类可以理解成集合的概念, 这个集合内部包含的每一个元素可以是由一个 case 组成, 也可以是由多个 case 组成这个类的实例当中 runnables 属性 (数组) 保存了需要串行执行的 case 或 case 组一个 case 可以当做一个组(runnables), 多个 case 也可以当做一组, AVA 用 Sequence 这个类来保证在 runnables 中保存的不同元素的顺序执行
顺序执行了解后, 我们再看下 AVA 内部实现的另外一个控制 case 并行执行的类: Concurrent:
- class Concurrent {
- constructor (runnables) {
- this.runnables = runnables
- }
- run () {
- // do something
- }
- }
可以将 Concurrent 可以理解为组的概念, 实例当中的 runnables 属性 (数组) 保存了这个组中所有待执行的 case 这个 Concurrent 和上面提到的 Sequence 组都部署了 run 方法, 用以 runnables 的执行, 不同的地方在于, 这个组内的 case 都是并行执行的
具体到我们提供的实例当中:
A -->> B -->> (C | D)
,AVA 是如何从这 2 个类来实现他们之间的按序执行的呢?
在你定义 case 的时候:
- ava.serial('A', async () => {
- // do something
- })
- ava.serial('B', async () => {
- // do something
- })
- ava('C', async () => {
- // do something
- })
- ava('D', async () => {
- // do something
- })
在 ava 内部便会维护一个 serial 数组用以保存顺序执行的 case,concurrent 数组用以保存并行执行的 case:
- const serial = ['A', 'B'];
- const concurrent = ['C', 'D']
然后用这 2 个数组, 分别实例化一个 Sequence 和 Concurrent 实例:
- const serialTests = new Sequence(serial)
- const concurrentTests = new Concurrent(concurrent)
这样保证了 serialTests 内部的 case 是顺序执行的, concurrentTests 内部的 case 是并行执行的但是如何保证这 2 个实例 (serialTests 和 concurrentTests) 之间的顺序执行呢? 即 serialTests 内部 case 顺序执行完后, 再进行 concurrentTests 的并行执行
同样是使用 Sequence 这个类, 实例化一个 Sequence 实例:
const allTests = new Sequence([serialTests, concurrentTests])
之前我们就提到过 Sequence 实例的 runnables 属性中就维护了串行执行的 case, 所以在这里的具体体现就是, serialTests 和 concurrentTests 之间是串行执行的, 这也对应着:
A -->> B -->> (C | D)
接下来, 我们就具体看下对应具体的流程实现:
allTests 是所有这些 case 的集合, Sequence 类上部署了 run 方法, 因此调用:
allTests.run()
开始 case 的执行在 Sequence 类的 run 方法当中:
- class Sequence {
- constructor (runnables) {
- this.runnables = runnables
- }
- run () {
- // 首先获取 runnables 的迭代器对象, runnables 数组保存了顺序执行的 case
- const iterator = this.runnables[Symbol.iterator]()
- let activeRunnable
- // 定义 runNext 方法, 主要是用于保证 case 执行的顺序
- // 因为 ava 支持同步和异步的 case, 这里也着重分析下异步 case 的执行顺序
- const runNext = () => {
- // 每次调用 runNext 方法都初始化一个新变量, 用以保存异步 case 返回的 promise
- let promise
- // 通过迭代器指针去遍历需要串行执行的 case
- for (let next = iterator.next(); !next.done; next = iterator.next()) {
- // activeRunnable 即每一个 case 或者是 case 的集合
- activeRunnable = next.value
- // 调用 case 的 run 方法, 或者 case 集合的 run 方法, 如果 activeRunnable 是一个 case, 那么就会执行这个 case, 而如果是 case 集合, 调用 run 方法后, 还是对应于 sequence 的 run 方法
- // 因此在调用 allTests.run()的时候, 第一个 activeRunnable 就是'A',B2 个 case 的集合(sequence 实例)
- const passedOrPromise = activeRunnable.run()
- // passedOrPromise 如果返回为 false, 即代表这个同步的 case 执行失败
- if (!passedOrPromise) {
- // do something
- } else if (passedOrPromise !== true) { // !!! 注意这里, 如果 passedOrPromise 是个 promise, 那么会调用 break 来跳出这个 for 循环, 进行到下面的步骤, 这也是 sequence 类保证 case 顺序执行的关键
- promise = passedOrPromise
- break;
- }
- }
- if (!promise) {
- return this.finish()
- }
- // !!! 通过 then 方法, 保证上一个 promise 被 resolve 后 (即 case 执行完后), 再进行后面的步骤, 如果 then 接受 passed 参数为真, 那么继续调用 runNext() 方法再次调用 runNext 方法后, 通过迭代器访问的数组: iterator 迭代器的内部指针就不会从这个数组的一开始的起始位置开始访问, 而是从上一次 for 循环结束的地方开始这样也就保证了异步 case 的顺序执行
- return promise.then(passed => {
- if (!passed) {
- // do something
- }
- return runNext()
- })
- }
- return runNext()
- }
- }
具体到我们提供的例子当中:
allTests 这个 Sequence 实例的 runnables 属性保存了一个 Sequence 实例 (A 和 B) 和一个 Concurrent 实例(C 和 D)
在调用 allTests.run()后, 在对 allTesets 的 runnables 的迭代器对象进行遍历的时候, 首先调用包含 A 和 B 的 Sequence 实例的 run 方法, 在 run 内部递归调用 runNext 方法, 用以确保异步 case 的顺序执行
具体的实现主要还是使用了 Promise 迭代链来完成异步任务的顺序执行: 每次进行异步 case 时, 这个异步的 case 会返回一个 promise, 这个时候停止迭代器对象的遍历, 而是通过在 promise 的 then 方法中递归调用 runNext(), 来保证顺序执行
- return promise.then(passed => {
- if (!passed) {
- // do something
- }
- return runNext()
- })
当 A 和 B 组成的 Sequence 执行完成后, 才会继续执行由 C 和 D 组成的 Conccurent, 接下来我们看下并发执行 case 的内部实现: 同样在 Concurrent 类上也部署了 run 方法, 用以开始需要并发执行的 case:
- class Concurrent {
- constructor(runnables, bail) {
- if (!Array.isArray(runnables)) {
- throw new TypeError('Expected an array of runnables');
- }
- this.runnables = runnables;
- }
- run() {
- // 所有的 case 是否通过
- let allPassed = true;
- let pending;
- let rejectPending;
- let resolvePending;
- // 维护一个 promise 数组
- const allPromises = [];
- const handlePromise = promise =>{
- // 初始化一个 pending 的 promise
- if (!pending) {
- pending = new Promise((resolve, reject) =>{
- rejectPending = reject;
- resolvePending = resolve;
- });
- }
- // 如果每个 case 都返回的是一个 promise, 那么首先调用 then 方法添加对于这个 promise 被 resolve 或者 reject 的处理函数,(这个添加被 reject 的处理, 主要是用于下面 Promise.all 方法来处理所有被 resolve 的 case)同时将这个 promise 推入到 allPromises 数组当中
- allPromises.push(promise.then(passed =>{
- if (!passed) {
- allPassed = false;
- if (this.bail) {
- // Stop if the test failed and bail mode is on.
- resolvePending();
- }
- }
- },
- rejectPending));
- };
- // 通过 for 循环遍历 runnables 中保存的 case
- for (const runnable of this.runnables) {
- // 调用每个 case 的 run 方法
- const passedOrPromise = runnable.run();
- // 如果是同步的 case, 且执行失败了
- if (!passedOrPromise) {
- if (this.bail) {
- // Stop if the test failed and bail mode is on.
- return false;
- }
- allPassed = false;
- } else if (passedOrPromise !== true) { // !!! 如果返回的是一个 promise
- handlePromise(passedOrPromise);
- }
- }
- if (pending) {
- // 使用 Promise.all 去处理 allPromises 当中的 promise 当所有的 promise 被 resolve 后才会调用 resolvePending, 因为 resolvePending 对应于 pending 这个 promise 的 resolve 方法, 也就是 pending 这个 promise 也被 resolve, 最后调用 pending 的 then 方法中添加的对于 promise 被 resolve 的方法
- Promise.all(allPromises).then(resolvePending);
- // 返回一个处于 pending 态的 promise, 但是它的 then 方法中添加了这个 promise 被 resolve 后的处理函数, 即返回 allPassed
- return pending.then(() =>allPassed);
- }
- // 如果是同步的测试
- return allPassed;
- }
- }
- }
具体到我们的例子当中: Concurrent 实例的 runnables 属性中保存了 C 和 D2 个 case, 调用实例的 run 方法后, C 和 D2 个 case 即开始并发执行, 不同于 Sequence 内部通过 iterator 遍历器来实现的 case 的顺序执行, Concurrent 内部直接只用 for 循环来启动 case 的执行, 然后通过维护一个 promise 数组, 并调用 Promise.all 来处理 promise 数组的状态
以上就是通过一个简单的例子介绍了 AVA 内部的流程控制模型简单的总结下:
在 AVA 内部使用 Promise 来进行整个的流程控制(这里指的异步的 case)
串行:
Sequence 类来保证 case 的串行执行, 在需要串行运行的 case 当中, 调用 Sequence 实例的 runNext 方法开始 case 的执行, 通过获取 case 数组的 iterator 对象来手动对
case(或 case 的集合)
进行遍历执行, 因为每个异步的 case 内部都返回了一个 promise, 这个时候会跳出对 iterator 的遍历, 通过在这个 promise 的 then 方法中递归调用 runNext 方法, 这样就保证了 case 的串行执行
并行:
Concurrent 类来保证 case 的并行执行, 遇到需要并行运行的 case 时, 同样是使用 for 循环, 但是不是通过获取数组 iterator 迭代器对象去手动遍历, 而是并发去执行, 同时通过一个数组去收集这些并发执行的 case 返回的 promise, 最后通过 Promise.all 方法去处理这些未被 resolve 的 promise, 当然这里面也有一些小技巧, 我在上面的分析中也指出了, 这里不再赘述
关于文中提到的 Promise 进行异步流程控制具体的应用, 可以看下这 2 篇文章:
Promise 异步流程控制 Node.js 设计模式基于 ES2015 + 的回调控制流
来源: https://juejin.im/post/5ab4d47c5188251fc32935e4