AVA测试框架内部的Promise异步流程控制模型
CommanderXL opened this issue · 1 comments
最近将内部测试框架的底层库从mocha
迁移到了AVA
,迁移的原因之一是因为AVA
提供了更好的流程控制。
我们从一个例子开始入手:
有A
,B
,C
,D
4个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',‘B’2个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
和D
2个case
,调用实例的run
方法后,C
和D
2个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篇文章:
我什么时候才能像你这样优秀,大佬