一、背景
先说明一下我在实际开发中遇到的问题:
在vue框架下,写页面启动初始化mounted()的时候,需要先查A下拉框的值,再将A中第一个元素a1设为A下拉框的默认值;再通过a1作为查询条件去查B下拉框的值,将B中第一个元素b1设为B下拉框的默认值;然后再通过a1和b1+c1(C下拉框第一个元素,是一个枚举,不需要查询后台),一起作为查询条件去后台进行一次搜索。
二、问题及解决
声明一下:这个部分只负责提出问题和解决问题,至于为什么这么做在后续章节会给出我的个人理解
2.1 初始版本
这里先把上述问题的函数先分别定义一下,再在每个函数进入位置加个输出:
searchA(){ //查询并设置第一个值a1为A下拉框默认值
console.log("A");
},
searchB(){ //查询并设置第一个值b1为B下拉框默认值
console.log("B");
},
setC(){ //设置第一个值c1为C下拉框默认值
console.log("C");
},
searchAll(){ //通过所有查询条件进行一次查询
console.log("D");
}
首先是第一个版本的代码:
mounted(){
searchA();
searchB();
setC();
searchAll();
}
激动人心的时刻到了,输出是什么呢?
输出:A B C D;
okk,一切和预期一模一样,完美,收工,下班,完结撒花!!!
唯一的一点点小缺憾就是代码跑出来的结果不对(︶︿︶),没办法,接着加班吧
2.2 异步版本1
为什么上面输出顺序看起来没问题,但是实际上执行结果不对呢?是因为输出的位置都在函数入口处,没有写在调用的异步函数里面,看到的输出其实是表象,还没真正调用干活的函数呢,那么对前面版本的模型进行改进,把输出放到异步函数里面:
searchA(){
return asyncA().then(() => {
console.log("A");
});
},
searchB(){
return asyncB().then(() => {
console.log("B");
});
},
setC(){
console.log("C");
},
searchAll(){
return asyncAll().then(() => {
console.log("D");
});
}
那么现在程序的输出结果是什么呢?
输出:C A D (B); (这里的B实际上这个位置上出现的是报错信息,并没有输出B)
那么为什么这里B报错信息出现的位置在A之后还会报错呢(问题一):B开始执行的时候A还没结束执行,所以B会报错。(问题一的详细原因后文会具体解释)
2.3 让B在A执行结束后再开始执行
为了让A执行结束后再执行B,把B从外面塞到A里面去:
mounted(){
searchA();
setC();
searchAll();
},
searchA(){
return asyncA().then(() => {
console.log("A");
searchB();
});
},
输出:C A D B;
虽然是不报错了,但是调用查询的D在设置查询条件B之前就执行了,B还是做了无用功;
2.4 所有异步任务都等等A吧
最后强制所有异步任务都等A干完才允许执行:
async mounted(){
await searchA();
setC();
searchAll();
}
输出:A C B D;
可以正常工作了,可是C还在B前面,可让我一个强迫症太难受了,这可怎么办呀T^T (问题二)
2.5 逼死强迫症
在A调用B的时候加个 “return” ;
searchA(){
return asyncA().then(() => {
console.log("A");
return searchB();
});
}
输出:A B C D;
可算是让强迫症舒服了!
PS:但是从执行效率上来看,只要A B D的相对顺序和C D的相对顺序确定了就行,A B C D并不是最高执行效率的方法。
三、任务执行顺序
- 首先先明确任务分同步任务和异步任务;
- 执行程序时先顺序执行同步任务,再执行异步任务;
- 异步任务又分为宏任务和微任务;
- 从宏任务队列中取出一个宏任务执行前需要先按顺序执行完所有微任务;
- 重点:宏任务进入宏任务队列并不是按照代码顺序进入的。(问题一可以证明微任务也是同理)(个人根据实验结果理解,如果有误希望大佬们帮忙指正,具体见下文)
四、宏任务和微任务
4.1 宏任务
宏任务(Macro Tasks)包括以下几种:
主代码块:JavaScript中的主线程代码。
setTimeout和setInterval:定时器任务。
I/O操作:例如文件读写、网络请求等。
UI渲染:浏览器需要重绘或者重新布局的任务。
4.2 微任务
微任务(Micro Tasks)包括以下几种:
Promise回调函数:Promise对象的处理程序(then、catch、finally)。
MutationObserver:监测DOM变化的任务。
process.nextTick(Node.js环境下):在当前"执行栈"结束后立即执行的任务。
4.1和4.2参考:
4.3 宏任务队列
这个问题是我在解决完问题后看到一篇博客后产生的,文中有这样一个栗子(文中文件读写的方法我电脑上没办法运行,换了种写法代替):
let fs = require('fs')
console.log('A')
fs.readFile('./1.js',(err,data) =>{
console.log('B')
})
setTimeout(() =>{
console.log('C')
},0)
console.log('D')
作者说正确的输出结果为:A D C B,是因为C的回调函数的延时时间为0因此其会作为异步任务(耗时任务)的第一个执行对象!!
我看到这我的好奇心起来了,如果setTimeout函数的延时不是0会怎么样?
我一点点的改变参数,0 1 2的输出都是A D C B,当增加到3的时候,输出结果变成了A D B C,为什么?(问题一)
我问了一些有经验的前辈,得到的解释是这个3就是读文件所需要的时间,先读完文件了,就先执行B,反之就先执行C;那么具体执行顺序和B文件大小有关。
同为宏任务,执行顺序是根据任务队列来的,可是这里为什么感觉执行起来顺序有点随机呢?
我一开始看完宏任务和微任务后只关心两者的相对执行顺序,知道要去各自的任务队列中取,但是就是不知道任务队列是怎么创建的。我针对我这一个盲点反复去找资料,最后把目光定格在一句话上“它们的回调函数被加入到任务队列中”。
我的理解为:领导分别给A B C分配了任务,任务队列是任务干完了,给领导汇报的顺序,C先干完C就先排队汇报;而不是给A B C分配任务的顺序作为任务队列。
本例的问题就很好解释了,是文件先读取完还是延时先到,就先执行哪个。由于延时是0的时候,读取文件要一定时间,那么输出A D C B就是必然。当延时不为0的时候就要看文件大小了,输(执)出(行)结(顺)果(序)就具有不确定性。
接着解释一下问题一:A确实比B先开始执行,可是B开始执行的时候A还没读取回来列表,将a1设为默认值呢,也就是B的查询条件不足,报错也就跟着发生了。
五、async和await
这部分我仅仅用于解答问题二,遇到其他情况了我再针对问题具体分析。
为什么加了一个return 就能让B插队而不用到任务队列后面去排队了?
还是刚才那篇博客,开篇加粗就写明了“在使用await关键字时,程序的执行将暂停,直到该关键字后面的Promise对象完成执行并返回结果。”如果它返回的结果是一个函数,那么要等这个函数执行完返回结果了,大家才能继续干活,也才让程序的执行顺序达到了预期的结果。