Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVA测试框架内部的Promise异步流程控制模型 #29

Open
CommanderXL opened this issue Mar 4, 2018 · 1 comment
Open

AVA测试框架内部的Promise异步流程控制模型 #29

CommanderXL opened this issue Mar 4, 2018 · 1 comment

Comments

@CommanderXL
Copy link

最近将内部测试框架的底层库从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+的回调控制流

@itkasumy
Copy link

itkasumy commented Mar 5, 2018

我什么时候才能像你这样优秀,大佬

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants