AVA測試框架內部的Promise非同步流程控制模型

菠蘿小蘿蔔發表於2018-03-23

作者:肖磊

個人主頁:github

最近將內部測試框架的底層庫從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屬性(陣列)儲存了需要序列執行的casecase組。一個case可以當做一個組(runnables),多個case也可以當做一組,AVASequence這個類來保證在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個陣列,分別例項化一個SequenceConcurrent例項:

const serialTests = new Sequence(serial)
const concurrentTests = new Concurrent(concurrent)
複製程式碼

這樣保證了serialTests內部的case是順序執行的,concurrentTests內部的case是並行執行的。但是如何保證這2個例項(serialTestsconcurrentTests)之間的順序執行呢?即serialTests內部case順序執行完後,再進行concurrentTests的並行執行。

同樣是使用Sequence這個類,例項化一個Sequence例項:

const allTests = new Sequence([serialTests, concurrentTests])
複製程式碼

之前我們就提到過Sequence例項的runnables屬性中就維護了序列執行的case,所以在這裡的具體體現就是,serialTestsconcurrentTests之間是序列執行的,這也對應著: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例項(AB)和一個Concurrent例項(CD)。

在呼叫allTests.run()後,在對allTesets的runnables的迭代器物件進行遍歷的時候,首先呼叫包含ABSequence例項的run方法,在run內部遞迴呼叫runNext方法,用以確保非同步case的順序執行。

具體的實現主要還是使用了Promise迭代鏈來完成非同步任務的順序執行:每次進行非同步case時,這個非同步的case會返回一個promise,這個時候停止迭代器物件的遍歷,而是通過在promisethen方法中遞迴呼叫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屬性中儲存了CD2個case,呼叫例項的run方法後,CD2個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的遍歷,通過在這個promisethen方法中遞迴呼叫runNext方法,這樣就保證了case的序列執行。

並行:

Concurrent類來保證case的並行執行,遇到需要並行執行的case時,同樣是使用for迴圈,但是不是通過獲取陣列iterator迭代器物件去手動遍歷,而是併發去執行,同時通過一個陣列去收集這些併發執行的case返回的promise,最後通過Promise.all方法去處理這些未被resolvepromise,當然這裡面也有一些小技巧,我在上面的分析中也指出了,這裡不再贅述。

關於文中提到的Promise進行非同步流程控制具體的應用,可以看下這2篇文章:

Promise 非同步流程控制 《Node.js設計模式》基於ES2015+的回撥控制流

相關文章