js事件循环的理解

102 阅读7分钟

声明:此文章摘取借鉴自一个知识点汇总网站:https://www.yuque.com/baiyueguang-rfnbu/tr4d0i/dm2emf

前端面试经常出现事件循环的问题,可能出现的问题主要有下面几个:

  1. 事件循环的概念和原理。
    • 什么是事件循环
    • 事件循环解决了什么问题
    • 事件循环的完整过程
  1. 宏任务和微任务的概念。
    • 什么是宏任务和微任务
    • 为什么要区分宏任务和微任务
  1. 说出代码片段执行结果。
    • 异步代码执行顺序

知识点

什么是事件循环?

JavaScript是单线程的,为避免单线程中代码执行阻塞,JavaScript通过事件循环机制支持异步操作。

简言之,事件循环是JavaScript引擎的一种任务执行机制,用来支持异步操作。

同步任务和异步任务

为什么需要支持异步操作呢?

写的代码语句对于JavaScript引擎来说就是一个一个的任务。我们有时候希望代码一条语句接着一条语句执行(同步执行),有时候某些代码执行需要等待一段时间,我们希望先执行后续代码,等异步代码到达时机再执行(异步执行)。

举个例子

console.log(1)
setTimeout(() => {
  console.log(2)
}, 1000)
console.log(3)

上述代码我们希望先打印1,然后延时1s打印2语句不会阻塞后语句执行,先打印3,等1s之后再打印2

对于上面代码,打印13是同步任务,一条语句接着一条语句执行。而打印1的是异步任务,它不阻塞后续代码,而是先执行后续代码,等异步任务达到时机(定时器到达时间)再执行。

那么浏览器是如何通过事件循环机制支持异步任务的呢?

事件循环原理

事件循环通过任务队列实现对异步任务的支持。JavaScript代码执行时候遇到异步任务,先交给其他线程(如定时器线程、网络请求线程),其他线程处理完,再把回调加入到JavaScript线程的任务队列中。

JavaScript线程会不断地轮询任务队列,发现有任务就取出来执行,这个循环的过程就称为事件循环。

console.log(1)
// 交给定时器线程计时,并继续执行后续代码
// 计时器线程结束后会把回调(打印2的函数)加入到任务队列中,JavaScript循环检测时候发现该任务则会执行
setTimeout(() => {
  console.log(2)
}, 1000)
console.log(3)

异步任务有2种,一种是有一些任务需要其他模块支持,并且需要在其他模块处理完再把回调交给JavaScript线程执行。比如定时器、网络请求。

还有一种是JavaScript本身支持的,在当前代码执行过程中,希望创建一个任务,这个任务不影响后续代码执行,而是等当前代码执行完再执行。比如Promise、MutationObserver。

console.log(1)
Promise.resolve()
// 打印2的函数将被加入到任务队列中,然后继续执行后续代码。任务队列中的人物在后续轮询中被执行
.then(() => {
  console.log(2)
})
console.log(3)

这两种异步任务前者被称为宏任务,后者被称为微任务。

为什么会有这两种类型的异步任务,在下面的章节中会详细说明。

总之,事件循环机制通过任务队列来支持异步任务,遇到异步任务交给其他线程执行或者加入到任务队列,然后继续执行后续代码,然后当前代码执行完后再从任务队列中取任务执行,这样就保证了异步任务不会阻塞后续代码的执行了。

事件循环的完整过程

事件循环的过程为:当执行栈空的时候,就会从任务队列中,取任务来执行。共分3步:

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

每个循环称为一个tick。

可以简单描述为:一个宏任务 + 所有微任务 ->一个宏任务 + 所有微任务……,循环往复。

(其中,UI渲染会根据浏览器的逻辑,决定要不要马上执行更新,不一定在本次循环中立即执行。可以看到,事件循环中包含UI渲染,这就是为什么我们说JavaScript的执行会阻塞UI渲染。)

我们看一个例子

console.log(1);
new Promise(resolve => {
  resolve();
  console.log(2);
}).then(() => {
  console.log(3);
})
setTimeout(() => {
  console.log(4);
}, 0);
console.log(5);

我们用上面讲的事件循环过程分析一下代码执行过程

0. 初始状态

代码都在调用栈中

1. 第一步

执行当前调用栈,先打印1,然后执行new Promise,打印2,然后将.then回调放到微任务队列,将setTimeout回调放到宏任务队列,然后打印5,调用栈为空

打印1,2,5

2. 第二步

查看微任务队列,取出promise.then的回调放入调用栈中执行,执行完后调用栈为空。

打印3

3. 第三步

微任务队列为空,所以查找宏任务队列中的setTimeout回调,放入调用栈中,执行完后为空。

打印4

4. 结束

调用栈为空,执行结束

宏任务和微任务

为什么区分宏任务和微任务?

为了区分优先级。

我们已经知道,异步任务分为依赖外部线程的异步任务和JavaScript本身创建的异步任务。这两种异步任务的优先级是不同的。

JavaScript自己创建的异步任务应该优先执行,而不应该放到宏任务队列底部等待当前宏任务都执行完再执行。优先执行微任务,这样 微任务中所做的状态修改在下一轮事件循环中也能得到同步。

我们通过Promise和MutationObserver这两个例子来看下。

const promise = Promise.resolve()
promise
  .then(res => { console.log(1) })
	.then(res => { console.log(2) })
console.log(3)

上面这段代码中,promise注册了两个回调,而这时候promise状态已经变为fullfilled了,所以回调应该在下个tick之前就执行,而不应该等待其他宏任务执行完再执行。另外我们看到promise通过then注册了两个回调。如果不区分优先级,可能这两个then回调中间会被插入其他宏任务,这显然是没必要的。

再比如MutationObeserver:

<!DOCTYPE html>
<html>
<head>
    <title>demo</title>
</head>
<body>
    <div id="root">
    </div>
    <script>
        const callback = (mutataionList, observer) => {
            console.log(mutataionList[0].attributeName + ' was modified')
        }
        const observer = new MutationObserver(callback)
        observer.observe(document.querySelector('#root'), {
            attributes: true, childList: true, subtree: true
        })
        document.querySelector('#root').setAttribute('style', 'background-color: red')
        console.log('after modified')
    </script>
</body>
</html>

上面代码为DOM修改注册了一个回调,然后修改了DOM。

修改完DOM之后,在下个tick之前就应该执行回调,而不应该等后面宏任务都执行完再回调,因为每个tick都可能重新渲染界面,每个tick也可能修改DOM,所以当前修改DOM之后回调应该尽快执行。

总结一下,宏任务指依赖外部线程的异步任务,微任务指JavaScript本身创建的异步任务。区分宏任务和微任务是为了让JavaScript创建的任务更及时执行。

总结理解

1. 什么是事件循环?

事件循环是JavaScript引擎的一种任务执行机制,用来支持异步操作。避免阻塞后续代码执行。

2. 宏任务和微任务的区别?

  1. 宏任务队列依赖外部线程,是由外部线程加入到任务队列的;微任务不依赖外部线程,是JavaScript线程创建并加入到队列的。
  2. 执行优先级不同,每次tick取一个宏任务执行,然后再把微任务队列中所有的微任务都执行完。
  3. 宏任务队列可以有多个,微任务队列只有一个。
  4. 宏任务有 script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering。微任务有 process.nextTick, Promise, Object.observer, MutationObserver。
  5. 宏任务队列有优先级之分。每次js引擎从宏任务队列中取宏任务时,会按照优先级选择宏任务队列,若高优先级的宏任务队列中没有任务时,才会到低级的宏任务队列中去取任务。