直接搞定浏览器js运行机制和事件循环机制

写在前头
关于浏览器Event Loop我发现有很多到文章都没有说清楚,包括有很多小伙伴都感觉云里雾里。有的文章说宏任务是先于微任务执行,有的说是微任务先于宏任务执行,实际上这种矛盾就是因为前提没有说清楚,才会导致概念和代码运行打印不同。带着这个疑问,我们一起来探究,这篇文章一定会让你完全掌握浏览器的事件循环机制。
为什么要事件循环?
因为js是单线程,单线程意味着每次只能执行一个任务,如果遇到前面一个任务耗时很长,后面的任务就不得不一直等着,这样会造成阻塞,为了协调事件、用户交互、脚本、渲染和网络等,必须使用事件循环。
任务执行顺序?
js执行的任务有两种,一种是同步任务,一种是异步任务。
同步任务:会立即执行的任务
异步任务:不会立即执行的任务(异步任务又分为宏任务与微任务)。常见的异步任务:Ajax,Dom事件操作,setTimeOut,promise的then方法,Node读取文件
宏任务包括,script(整体代码), setTimeout, setInterval, setImmediate
微任务包括,process,nextTick(Nodejs), Promises.then,用户触发的点击事件,滚动事件,键盘事件


直接搞定浏览器js运行机制和事件循环机制
文章图片
javascript的运行机制:
1、所有同步任务都在主线程上执行,形成一个“执行栈”
2、主线程之外还存在一个“任务队列”,当事件被识别为异步任务的时候,浏览器会把这个任务放在 Event Table,这个Event Table会把传入的异步事件注册为一个回调函数,然后传给Event Queue,等待主线程执行。
3、那什么时候这个回调函数会被执行呢?
在js引擎中,存在一个monitoring process的进程,它会不断检查主线程的任务是否全部执行完成,一旦全部执行完成,就会去任务队列(Event Quene)检查有哪些待执行的函数。如果有,就把这个回调函数放在主线程上执行
4、而这个异步任务不断排队,主线程不断检查异步排队,主线程不断执行的循环就叫事件循环 Event Loop
需要注意的是:当前执行栈执行完毕时会立刻先处理掉微任务队列中的事件,然后再去宏任务取事件。当执行宏任务时,此刻微任务队列有新的事件进来,会先把其他宏任务放一边,先执行完微任务队列里的任务,再继续执行宏任务。
在一个任务队列中,可以有多个的宏任务队列,但是微任务队列只有一个.
同一次事件循环中,微任务优先于宏任务执行。前提是同一次事件循环中。
这句话是本章的重点。事实上我们可以看到很多文章都是说宏任务优先于微任务执行,这是对的。然而很多例子显示却不是如此,下面是正确理解的执行步骤。
1、从全局的script开始,任务依次入栈,遇到同步事件直接被主线程执行,若执行完则出栈,这包括promise事件,因为它是立即执行。遇到异步事件就放到任务队列区分宏任务和微任务
2、第一次同步任务执行完后,在本轮看看微任务有没有任务,有就执行,没有就进入下一轮执行宏任务
3、执行的宏任务里如果有微任务,就把其他宏任务放一边,先执行微任务
所以宏任务是先于微任务执行指的就是我们第一次运行的整体代码就是宏任务!接下来的每次循环都是微任务优先于宏任务!!
Promise的回调函数不是正常的异步任务,而是微任务(microtask)。区别在于,正常异步任务追加到下一轮事件循环,微任务追加到本轮事件循环。js中的代码执行顺序为:同步程序——promise微任务——异步程序。下面我们直接上代码演示,然后在下一章,我会仔细讲解ES6的promise、async和await的特点和用法,另外准备一些面试题给大家学习。
例子1:

console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一轮循环
1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2
第一轮循环打印出的是 1 7 8(这个8在本轮同步任务结束后执行微任务)
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:then1(本轮结束前执行所有微任务)
第二轮循环:
1)、微任务都执行结束了,开始执行第一个宏任务
2)、执行 setTimeout1 (第一个setTimeout)
3)、首先打印出 2
4)、new Promise中resolve 打印出 4
5)、then 微任务 记为 then2
当前宏任务队列:setTimeout2
当前微任务队列:then2(本轮结束前执行所有微任务)
第二轮循环结束,当前打印出来的是 1 7 8 2 4 5
第三轮循环:
1)、执行第一个宏任务,也就是执行 setTimeout2 (第二个setTimeout)
2)、首先打印出 9
3)、new Promise执行resolve 打印出 11
4)、then 微任务 记为 then3
当前宏任务队列为空
当前微任务队列:then3(本轮结束前执行所有微任务)
第三轮循环结束,最终打印出来的是 1 7 8 2 4 5 9 11 12
例子2:
console.log('第一轮');
setTimeout(() => {//为了便于叙述时区分,标记为 setTimeout1
console.log('第二轮');
Promise.resolve().then(() => {//为了便于叙述时区分,标记为 then1
console.log('A');
})
}, 0);
setTimeout(() => {//为了便于叙述时区分,标记为 setTimeout2
console.log('第三轮');
console.log('B');
}, 0);
new Promise((resolve)=>{//为了便于叙述时区分,标记为 Promise1
console.log("C")
resolve()
}).then(() => {//为了便于叙述时区分,标记为 then2
Promise.resolve().then(() => {//为了便于叙述时区分,标记为 then3
console.log("D")
setTimeout(() => {//为了便于叙述时区分,标记为 setTimeout3
console.log('第四轮');
console.log('E');
}, 0);
});
});
// 执行结果:第一轮 > C > D > 第二轮 > A > 第三轮 > B > 第四轮 > E


第一轮
宏任务:setTimeout1,setTimeout2,setTimeout3
微任务:then2,then3 (第一轮结束前清空微任务)
打印:第一轮,C,D
上面这里要注意,第一轮中微任务队列不为空,then2出队列并执行,然后这个微任务里的 then3继续进入微任务队列,只要微任务队列不为空,本轮就没有结束。执行then2的时候,then3和setTimeout3分别入列
第二轮
宏任务:setTimeout2,setTimeout3
微任务:then1(第一轮结束前清空微任务)
打印:第二轮,A
第三轮
宏任务:setTimeout3
微任务:
打印:第三轮,B,
第四轮
宏任务: 空
微任务:空
打印:第四轮,E
大家看看自己能否真正理解。不理解可以留言,我会解答并增加例子解释。这篇文章更正了很多文章的错误,同时集结其他文章的突出地方。下一篇讲promise,更多详细案例等你。
参考链接:
https://zhuanlan.zhihu.com/p/257069622
【直接搞定浏览器js运行机制和事件循环机制】https://segmentfault.com/a/1190000018181334

    推荐阅读