在讨论今天的主题之前,大家需要明白一下几点概念
JS引擎线程:解释执行JS代码、用户输入、网络请求等
GUI线程(渲染线程):绘制用户界面、与JS主线程互斥
HTTP网络请求线程:处理用户的GET、POST等请求,等拿到返回结果后,将回调函数推入事件队列
定时器触发线程:setTimeout、setInterval等待时间结束后,将执行函数推入事件队列中
事件处理线程:将click、mouse、input等交互事件发生后,将事件处理函数推入事件队列中
JS主线程执行代码的空间,JS在每解释一行代码就会将代码放入栈底,执行时又从栈顶依次执行;一个函数执行结束后遇到下一个函数又开始重复解释进栈、出栈执行。
function foo(){
console.log('foo');
function bar(){
console.log('bar');
}
bar();
}
foo();
(执行栈顶) | bar函数 | foo函数 ( 执行栈底) |
解释:foo函数会被先放入栈底,其次bar函数进入,执行时bar函数先出栈即先执行,然后foo函数出栈并执行
内包含HTTP网络请求线程、定时器触发线程、事件处理线程,帮助JS主线程做一些异步的事情,比如监听事件、计时等
一个内存空间,用于存放即将执行的异步处理函数(比如定时器计时结束、click事件被点击),当JS引擎空闲(执行栈没有可执行的上下文时),会从事件队列中拿出第一个函数执行
(队列头)函数1 | 函数2 | 函数3(队列尾) |
解释:函数1先进入事件队列,函数3最后进入事件队列,执行时函数1先出队并执行,依次是函数2和函数3
知道这几个概念和流程之后来看个小例子,看他们是如何在实际情况中运行的
例1:
console.log(1);
function foo(){
console.log(2);
setTimeout(function (){
console.log(3);
},0)
}
foo();
console.log(4);
执行结果
执行过程解析:
由于console.log()是同步执行的所以会率先打印1(这里也可以看做进入了执行栈只不过就一条语句执行结束后就退出了),遇到foo,会将foo放入stack执行栈栈底,foo执行将console.log()函数推入执行栈倒数第二位,执行输出2,再次执行遇见了setTimeou()放入了执行栈倒数第三位,但是JS引擎发现它需要异步执行,于是JS引擎将setTimeout推入宿主环境由宿主环境的定时器线程进行监听,这时Js主线程继续执行同步代码,打印4,当定时器线程计时0毫秒结束后,立即将setTimeout内的console.log()函数推入事件队列,等待执行(如果JS主线程的同步任务代码没有执行完则等待,执行结束则执行事件队列的任务),由于此时同步任务已经执行完毕,JS引擎则会提取事件队列头部的任务,进而打印3。
例2:
setTimeout(()=>{
console.log(1);
},0)
new Promise((resolve,reject)=>{
console.log(2);
resolve();
}).then(()=>{
console.log(3);
})
console.log(4);
//2、4、3、1
为什么Promise.then的3先打印出来呢?同样都是异步‘事件’按照执行顺序setTimeout()先被放到了宿主环境监听不是应该2、4、1、3吗?
这是因为即使同样是异步事件但也有‘主次之分’,这个主次就是宏任务和微任务
宏任务(macro task):用户交互(事件)、setTimeOut|setInterval、网络事件、history traversal(h5中的历史操作)、I/O、setImmediate(Nodejs环境)、script标签
微任务(micro task):Promise、MutaionObserver(h5中用来监听dom结点变化)、process.nextTick(Nodejs)
宏任务会被放入宏任务队列(宏事件队列),微任务会被放入微任务队列,这里你不可以粗略的理解为微任务执行的优先级高于宏任务。
宏任务较微任务会被优先放置在自己的队列中但不代表会被优先执行。
宏任务微任务执行顺序是,当每一次事件循环开始时:先执行完主线程中的同步任务,取出微任务队列中的任务直至清空,然后拿出宏任务队列中的一个任务(注意这里是一个任务)执行,再执行微任务队列中全部任务。
进而重复上述执行顺序,执行一个宏任务,清空微任务,直至所有任务全部执行完成。
这样上面的问题就不难解答了,由于Promise是宏任务而setTimeout是微任务,JS同步任务执行完成之后就会先清空微任务队列(先执行Promise),再执行一个宏任务(setTimeout)。
当然本次讨论还没有结束,细心的人一定发现宏任务和微任务的执行顺序我们还没有体验什么是循环执行,上述例子仅执行了两步就结束了,还不足以证明这个结论。
例3:
console.log(1);
setTimeout(() =>{
console.log(5)
},100)
new Promise(resolve => {
console.log(2);
setTimeout(() => {
console.log(6)
}, 100);
resolve();
}).then(res => {
console.log(4)
})
console.log(3)
//执行顺序自然是1,2,3,4,5,6
为什么第一个setTimeout先于Promise内的setTimeout执行呢?不是说Promise是微任务先执行吗?
因为即使放入Promsie微任务内的setTimeout宏任务,它还是宏任务并不会进入微任务队列,还是在宏任务队列等待执行,又是后于第一定时器进入队列的自然也就最后执行。
例4:
console.log('执行顺序(1)')
var j = 0;
for (var i = 0; i < 5; i++) {
setTimeout(() => {//闭包
j++;
if (j <= 2) {
console.log("执行顺序(5、6) " + i + ' setTimeout_one');
} else {
console.log("执行顺序(10、11、12) " + i + ' setTimeout_one');
}
},i*1000);
console.log("执行顺序(2) for_console.log" + i)
}
new Promise(function (resolve, reject) {
console.log("执行顺序(3) " + 'Promise_one')
resolve();
}).then(function () {
console.log("执行顺序(4) " + 'Then_one')
})
setTimeout(function () {
console.log("执行顺序(7) " + 'setTimeout_two')
new Promise(function (resolve) {
console.log("执行顺序(8) " + 'setTimeout_two_Pormise')
resolve();
}).then(function () {
console.log("执行顺序(9) " + 'setTimeout_two_Then')
})
}, 1000)
这里只解释为什么会连续打印两个第一个setTimeout中的才执行后面的语句:第五次打印是因为第二个setTimeout的出现,进入宏任务队列,先执行第一个setTimeout的第一次打印,而为什么第一个setTimeout中第二次打印会紧接着打印呢,就是上面我们说宏任务队列和微任务队列执行顺序问题,此时微任务队列已经清空,会拿出宏任务队列中的一个进行执行,所以第二次会紧接着打印,此时由于第二个setTimeout的执行又向微任务队列添加了任务,所会等所有微任务清空后才会执行后序循环中的三个setTimeout().
今天的讨论就介绍到这里,编写不易,有用麻烦点个赞。