导语
JavaScript 是一门单线程语言(通俗的说就是一个时间内,只干一件事情),异步操作都是放到事件循环队列里面,等待主执行栈来执行,并没有专门的异步执行线程。
既然说一个时间只干一件事,那我们该如何解释下面的问题呢?
提出问题
<!-- 此处省略 css 部分-->
<body>
<section>
<div class="process" id="process-one"></div>
<div class="process" id="process-two"></div>
<button onclick="start()" style="margin-top: 20px">start</button>
</section>
</body>
<script>
function handle(dom) {
let i = 0;
(function run() {
dom.innerHTML = i
dom.style.width = i + '%'
if (++i <= 100) {
setTimeout(run, 50)
}
})()
}
let processOne = document.getElementById('process-one')
let processTwo = document.getElementById('process-two')
function start(){
handle(processOne)
handle(processTwo)
}
</script>
可以看到,当点击start 按钮之后,两个进度条会同时进行加载?
接下来我们通过下面的学习,来完美的解释这一现象。
名词解释
在解释这一系列名词之前我们先来看一张图
大致分为四个模块:
执行栈(Stack)
优先执行主线程代码,当主线程代码全部执行完成之后,会从任务队列当中优先拿取微任务队列中的任务,当微任务队列全部执行完成之后,会从宏任务队列中拿取任务
注意:如果宏任务执行过程中,又产生了微任务,还是会优先执行微任务代码
宏任务(macrotask)
主要包括:
script(整体代码)setTimeout,setInterval,setImmediate,I/O,ui rendering
在新标准中叫 task
微任务(microtask)
主要包括:
process.nextTick(NodeJs)PromiseMutationObserver(html5 新特性)
在新标准中叫 jobs
Time 模块
主要用于setTimeout、setInterval读秒
代码执行
概念性的东西就这么多,下面我们用代码结合图片的方式来解释整个事件轮询机制
根据下面执行栈内的代码,写出输出顺序
下面写出具体流程:
- 执行代码 ① 输出打印 1
- 执行代码 ② ,将
setTimeout放入Time模块开始读秒(当前时间为0,在现代浏览器当中,最少为4ms,也就是说哪怕写的是0,也会有4ms的读秒时长)。 - 执行代码 ③ ,Promise为微任务,放入微任务队列。
- 执行代码 ④ ,输出打印 4
- 至此同步任务全部执行完毕,开始拿异步任务,首先从微任务开始,将第一个微任务的callback回调函数放入执行栈继续执行
- 执行代码块 ③ 的回调函数,输出打印 3
- 至此同步任务全部执行完毕,继续从微任务拿取,此时的微任务队列为空,时间循环机制会宏任务中拿取,将 代码块 ② 的回调函数放入 执行栈
- 执行从宏任务中拿过来的代码块②的回调函数,输出打印2
最终结果: 1 ,4, 3, 2
看完上面的执行过程,那么什么是事件循环(Event Loop)机制呢?实际上也就是上面流程反复执行的这样一个机制就叫做事件轮询(Event Loop)。
了解了上面的流程,恭喜你,整个事件轮询机制就这么简单
下面我们再来看看有一些需要注意,容易记错的地方
定时模块的运行时机
setTimeout(function(){
// 请问程序抵达此处时,共用时约多少秒
}, 1000)
JSON.parse('./index.json') // 假设此处解析需要 5 秒钟
这里做一些简单的解释:setTimeout 在放入Time模块后就会开始读秒,所以这里并不是 JSON 解析 5 之后才进行的读秒,而是在JSON 解析之前开始的读秒,所以等 JSON 执行完成之后,setTimeout 读秒早已经接数,回调函数被放入了 宏任务队列,会被立马执行
宏任务中包含微任务
下面再看看宏任务中包含了微任务的情况,看完上面的例子,试着写出下面代码的输出结果:
console.log(1)
setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
})
})
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
return 6;
}).then((data)=>{
console.log(data)
})
setTimeout(() => {
console.log(7);
})
console.log(8);
看看你的答案是下面那一组(A / B)
A:1,4,8,5,6,2,3,7
B:1,4,8,5,6,2,7,3
如果既不是A也不是B,那么恭喜你,得回到文章顶部重新看一遍
这里唯一让你感到疑惑 大概就是 3 和 7 的顺序
简单来说,就是在宏任务执行的过程中,又产生了微任务,此时是应该继续处理后续的宏任务,还是先处理微任务?
想要弄清楚这一点,其实很简单,这里记住一句话:所有异步任务在执行过程,都是从异步任务队列中将回调函数拿到了任务栈中来执行,此时执行完任务栈里面的代码后,还是会继续按照之前的方式,先从微任务中拿,再从宏任务拿
那我们上面的代码,setTimeout的回调函数在被放入执行栈执行的时候又产生了微任务,等待执行栈执行完成之后事件循环机制还是会按照约定的步骤先去微任务队列拿任务来执行。
所以正确答案是:A
整个 Event Loop 理解了之后实际上并不难,简单的总结写下来,加深印象!