本篇文章就让我们来清晰地理解一下js中的事件循环机制。
进程和线程
在理解JS事件循环机制之前,我们先要理解进程和线程的概念,计算机上的所有操作都是由cpu执行的,cpu将要执行的操作分为一个个任务,这些任务就可理解为一个个进程,而这些任务又分为一些更小的子任务,这些子任务就可称为线程。所以进程为CPU 在运行指令和保存上下文所需要的时间,而线程为执行一段指令所需要的时间。
比如:浏览器每开一个tab页,就是新开一个进程,而新开的这个进程又有渲染线程,js 引擎线程,http 线程等。进程和线程都大多都可以并发执行,但是渲染线程 和 js 引擎线程是互斥的,不能并发执行,因为如果两个线程同时操作DOM,可能会导致数据不一致和页面显示错误。
event-loop
我们知道js是单线程的,v8 引擎在运行 js 代码时,这个进程中只有一个线程会被开启,也就是说它只能干一件事。这个线程负责执行所有的 js 代码,并且管理事件循环(Event Loop),所以说会有异步的概念。关于异步juejin.cn/post/744268…
先看这样一段代码了解一下事件循环;
let a = 1
console.log(a);
setTimeout(function () {
let b = 2
console.log(b);
a++
setTimeout(function () {
b++
console.log(b);
}, 2000)
console.log(b);
}, 1000)
console.log(a);
让我们先来梳理一下这段代码,从上往下执行,第一个输出1,然后碰到setTimeout
是一个耗时的异步代码,会先将它挂起,先执行下面的同步代码输出1,执行完同步代码后再执行异步代码,也就是setTimeout
里面的代码,输出一个2,然后a++,又碰到一个异步setTimeout
代码,先挂起,执行下面的同步代码console.log(b);
输出2,然后执行再执行挂起的异步代码b++ console.log(b);
输出3。
这就展现了js中的事件循环,先同步再异步,异步里面再先同步再异步.....反复重复。
那么哪些是异步代码呢?
js中的代码分为同步代码和异步代码,而异步代码里面又分为微任务和宏任务,如下。
- 同步代码(有些耗时的代码也是同步代码)
- 异步代码
-
微任务:promise.then(),process.nextTick(),MutationObserver()
-
宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI-rendering
微任务和宏任务是两种不同类型的任务队列,这两种任务的划分主要是为了更好地管理和执行异步代码,以及确保某些任务能够在其他任务之前被优先执行,微任务其中一个重要用途就是用Promise解决异步。
console.log('Script start');
setTimeout(() => {
console.log('setTimeout callback'); // 宏任务
}, 1000);
Promise.resolve().then(() => {
console.log('Promise then callback'); // 微任务
});
console.log('Script end');
执行顺序:
- “Script start”(同步代码)
- “Script end”(同步代码)
- “Promise then callback”(微任务,在当前宏任务执行完毕后立即执行)
- “setTimeout callback”(宏任务,在下一个事件循环迭代中执行)
eventLoop执行步骤
在js中,一段代码的执行总是先同步再异步,然后先执行微任务再执行宏任务。具体为:
- 执行同步代码(这属于宏任务)
- 执行完同步后,检查是否有异步代码需要执行
- 执行所有的微任务
- 如果有需要,就渲染页面
- 执行宏任务,也是开启了下一次事件循环(回到步骤2)
我们来看以下代码加深对事件循环的理解;
console.log(1);
new Promise(function (resolve, reject) {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
先分析里面的宏任务和微任务;
首先代码先执行同步代码输出 1,2,然后碰到promise.then()
微任务,将其放入微任务队列中(里面包含宏任务1),然后碰到setTimeout
宏任务2,然后将宏任务2 (里面包含宏任务3)放入宏任务队列中,然后执行最后一行的同步代码输出 7,此时两个队列如下。
执行完了第七行同步代码后,先执行微任务队列中的微任务,输出 3后,又碰到一个宏任务1,然后将其放入宏任务队列,此时微任务队列执行完毕,再执行宏任务队列如下;
队列,先进先出,先放进去的先执行,先执行宏任务 2,输出 5,然后碰到宏任务 3,将其放入宏任务队列,宏任务2执行完毕出队列;
然后执行宏任务1,输出 4,再执行宏任务3,输出 6。归总一下,输出1 2 7 3 5 4 6
。
综上,事件循环的执行步骤先执行同步代码,再异步代码(里面先执行微任务,再宏任务),宏任务相当于下一次循环开头的同步代码。
await
浏览器对await 的执行提前了(await 后面的代码当成同步来执行),而且会将后续(下面)代码挤入微任务队列
接下来看看以下代码来理解;
console.log('script start');
async function async1() {
await async2() // 立即执行
console.log('async1 end'); // 去到了微任务队列
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
});
console.log('script end');
先来看上半部分。
执行完最后一行输出
script end
后,然后执行微任务队列(async1 end
,then1
,then2
),再执行宏任务队列(setTimeout
)。所以最后输出结果:
setTimeout 定时器执行的时间准吗?
有时面试也会被问到这个问题,答案是不准,原因如下;
setTimeout被执行时,浏览器会启动一个新的线程来计时,等到时间结束才将定时器的回条取出来执行(js 主线程将其取出),如果此时 js 还在执行同步代码,那么该回调就会被一直挂起,直到同步执行完毕微任务也执行完毕才执行该回调。
好了,关于JS事件循环机制就讲解到这里了,相信你看完会对JS中的事件循环机制有了更加清晰的认识,感觉本文对你有所帮助的话可以点点赞哦(●'◡'●)。