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

AVA测试框架内部的Promise异步流程控制模型作者:肖磊个人主页:github最近将内部测试框架的底层库从mocha迁移到了AVA,迁移的原因之一是因为AVA提供了更好的流程控制。我们从一个例子开始入手:有A,B,C,D4个case,我要实现A–>>B–>>(C|D),A最先执行,B等待A执行完再执行,最后是(C|D)并发执行,使用ava提供的API来完成case就是:constava…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

作者:肖磊

个人主页: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
})

复制代码

Jetbrains全家桶1年46,售后保障稳定

接下来我们就来具体看下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+的回调控制流

转载于:https://juejin.im/post/5ab4d47c5188251fc32935e4

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/230778.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 交流转直流降压、稳压电路「建议收藏」

    交流转直流降压、稳压电路「建议收藏」目录目录降压稳压原理图设计思路电路图及原件原理图理解成果展示降压稳压原理图桥式整流电容滤波电路(最常用电路,本博客电路):全波整流电容滤波电路:-二倍压整流电容滤波电路:设计思路电路图及原件原件:PCB洞洞板、220V~12V变压器、7805稳压管、二极管*4、1000uF电解电容、10uF电解电容*2、…

  • JAVA、Android环境搭建

    JAVA、Android环境搭建JAVA环境搭建一.JDK的下载与安装一.JDK的下载下载JDK在本地储存盘,并安装;二.JDK的安装(1)鼠标右键点击此电脑,进入属性,找到高级系统设置;(2)点击高级系统设置,找到环境

  • 电视处理器a53和a55哪个厉害(cortexa55处理器好吗)

    小编语:手机处理器从32位向64位的迁移过程中,功耗增加以及给手机厂商带来的散热成为一个很大的问题。目前Cortex-A57已经是64位手机处理器芯片的主流内核,它的功耗不降下来,更多手机功能的开发可能受限于系统总功耗而无法实现,ARM这次推出的Cortex-A35显然在这方面下了不少功夫,究竟能给手机厂商带来多少实惠,还要等终端产品推出后才见分晓……64位元处理器应用版图…

  • 史上最硬核的Linux命令大全,还不收藏? ❤️【通俗易懂,小白一看就会】「建议收藏」

    史上最硬核的Linux命令大全,还不收藏? ❤️【通俗易懂,小白一看就会】「建议收藏」目录????前言????命令汇总????文件管理1️⃣ls命令–显示指定工作目录下的内容及属性信息2️⃣cp命令–复制文件或目录3️⃣mkdir命令–创建目录4️⃣mv命令–移动或改名文件5️⃣pwd命令–显示当前路径????文档编辑1️⃣cat命令–在终端设备上显示文件内容2️⃣e…

    2022年10月21日
  • 阿里云mqtt服务器_阿里云ecs新手教程

    阿里云mqtt服务器_阿里云ecs新手教程概述本篇主要讲述使用MQTTX软件与阿里云进行连接,上篇文章open62541基于mqtt订阅发布中有有关MQTTX软件的下载以及使用。建立连接这里我们使用MQTTX与阿里云建立连接,阿里云地址:https://iot.console.aliyun.com/lk/summary/new这里我们进行注册以及实名认证后进行登录,登录后界面如下所示:一定要实名认证后才可以使用,使用支付宝实名认证很快也很简单登录后我们就可以开始操作了。添加产品点击公共用例后就会跳转到添加产品界面,如下图所

    2022年10月23日
  • IDEA 2020年最新激活码

    亲测,2020年3月10日 有效期到2022年 请复制以下内容: 激活码见: IDEA最新激活码

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号