作者:肖磊
個人主頁:github
最近將內部測試框架的底層庫從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篇文章: