首页与我联系

深入理解关于JavaScript 的 Event Loop 运行机制

By 前端达人
Published in 1-JavaScript
September 17, 2022
2 min read
深入理解关于JavaScript 的 Event Loop 运行机制

在这篇文章中会介绍以下内容:

  • engine runtime 和 call stack 简介(以 V8 引擎为例)
  • Event Loop 运行机制的详解
  • microtasks 和 macrotask 的执行顺序

engine runtime 和 call stack 简介

在 chrome 浏览器和 nodejs 里都是用 V8 引擎解析和运行 JS 代码,我们先来看下 V8 引擎的简化图:

上图中 Heap 是用来做内存分配,Call Stack是用来执行 JS 代码,由于 JS 是单线程所以只有一个Call Stack。实际我们写网页开发的时候,除了一些 JS 代码,我们还会大量用到:DOM 事件、AJAX(XMLHttpRequest)、setTimeout 等等一些异步事件。从上图可以看出,这些异步事件都没有在 V8 引擎里,事实上这些异步事件不属于 V8 引擎,而是属于浏览器,并且 DOM 事件、AJAX(XMLHttpRequest)、setTimeout 都分别有单独的线程来处理。由于Call Stack执行(JS 运行线程)和页面渲染线程是互斥的,如果所有的事情都由 V8 引擎处理,这样肯定会导致页面卡顿。

浏览器多线程和 callback 机制完美避免了页面卡顿的问题。DOM 事件、AJAX(XMLHttpRequest)、setTimeout 这些异步事件在各自单独的线程处理完以后,每个异步事件都有 callback 回调函数,V8 引擎再把这些回调函数放在Call Stack执行。上述整个运行机制可以称为是 runtime,可以简化如下图:

如上图所示,web 异步事件结束以后,会有 callback,然后 runtime 把这些 callback 事件放到Callback Queue里,一旦Call Stack所有的方法都执行完以后,Event Loop会依次把 Callback Queue里的回调函数放到Call Stack里执行。

Event Loop 运行机制的详解

Event Loop 实际上就是一个 job,用来检测 Call Stack 和 Callback Queue,一旦 Call Stack 里代码执行完以后,就会把 Callback Queue 里第一个 callback 函数放到 Call Stack 里执行。我们来看个例子:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 1000);

console.log('script end');

运行运行结果如下:

script start
script end
setTimeout

我们具体一步一步看下整个流程:

1,代码没有运行之前,Call Stack Callback Queue都是空的

2,把console.log('script start')加到 Call Stack

3,执行console.log('script start'),在 console 里打印出script start,执行结束后把它移出 Call Stack

4,把 setTimeout 放到 Call Stack

5, 执行 setTimeout,用 setTimout 线程执行 timeout 时间,Call Stack 中 setTimeout 执行结束,把它移出 Call Stack

6, 把console.log('script end')加到 Call Stack

7,执行console.log('script end'),在 console 里打印出script end

8,console.log('script end')执行结束,把它移出 Call Stack

9,1000毫秒以后,计时结束,把 callbackcb1函数放到 Callback Queue 里

10,此时 Callback Stack 是空的,Event Loop 把 cb1 拿到 Callback Stack 里

11,执行 cb1,cb1 里有console.log('setTimeout'),把console.log('setTimeout')放到 Call Stack 里

12,执行console.log('setTimeout'),在 console 里打印出setTimeoutconsole.log('setTimeout')执行结束,把它移出 Call Stack

13,cb1执行结束,把它移出 Call Stack

总结来说就是,JS 是单线程的,只有一个 Call Stack,浏览器是多线程的,并且 DOM 事件、AJAX(XMLHttpRequest)、setTimeout 都是有单独的线程处理。在这些异步事件结束,runtime会把它们的 callback 按顺序放到 Callback Queue 里,Event Loop 会检测 Call Stack,一旦它为空,就会把 Callback Queue 里的回调函数依次放到 Call Stack 里执行,直到 Callback Queue 为空。

microtasks 和 macrotask 的执行顺序

刚才用 setTimeout 为例,解释了JS中 Event Loop 机制是怎么运行的,也提到过 runtime 会把回调函数依次按时间先后顺序放到 Callback Queue 里,然后 Event Loop 再依次把这些回调函数放到 Call Stack 里运行。我们在浏览器 Console 运行以下代码,看下结果:

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

执行结果如下:

script start
script end
promise1
promise2
setTimeout

上述代码虽然 setTimeout 延时为0,其实还是异步的。因为H5标准规定 setTimeout 函数的第二个参数不能小于4毫秒,不足会自动增加。

setTimeout 和 promise 都是异步事件,而且setTimeout 写在 promise 之前,为什么 setTimeout 的回调要比 promise 后执行呢?那是因为 promise 属于微任务(microtasks)而 setTimeout 属于宏任务(macrotask),微任务(microtasks)的优先级要高于宏任务(macrotask)。

首先我们需要明白以下几件事情:

  • JS 分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。

Callback Queue(Task Queue)里的回调事件称为宏任务(macrotask),每次异步事件结束后,它们的回调函数会依次按时间顺序放在 Callback Queue 里,等待 Event Loop 依次把它们放到 Call Stack 里执行。比如:setInterval setTimeout script setImmediate I/O UI rendering就是宏任务(macrotask)。

微任务(microtasks)是指异步事件结束后,回调函数不会放到 Callback Queue,而是放到一个微任务队列里(Microtasks Queue),在 Call Stack 为空时,Event Loop 会先查看微任务队列里是否有任务,如果有就会先执行微任务队列里的回调事件;如果没有微任务,才会到 Callback Queue 执行回到事件。比如:promise process.netTick Object.observe MutationObserver就是微任务(microtasks)。

在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。

整个 Event Loop 的执行顺序如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取,也就是 callbacke queue)

流程图如下:

我们再把代码改一下,在创建 promise 的时候,加一行console.log('Promise'),而且在第一个 promise resolve 的时候再加一个 setTimeout,代码如下:

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

new Promise(resolve => {
    console.log('Promise');
    resolve();
}).then(function () {
    setTimeout(function () {
        console.log('setTimeout in promise1');
    }, 0);
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

/**
script start
Promise
script end
promise1
promise2
setTimeout
setTimeout in promise1
**/

console.log('Promise')在这里是同步代码,console.log('script end')是同步代码且放在最后,所以Promisescript end前面,而且在微任务(microtasks)里有宏任务(macrotask),macrotask 还是会依次被放到 Callback Queue 等待执行。

如果有 async await 呢?再来看一段代码:

//请写出输出内容
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

/**
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
**/

我们知道 Promise 中的异步体现在 then 和 catch 中,所以写在 Promise 中的代码是被当做同步任务立即执行的。而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的。那么出现了 await 时候发生了什么呢?

由于因为 async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是 microtask。所以对于上面代码中的

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等价于:

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

我们来看一个变式, 将 async2 中的函数也变成了 Promise 函数:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

/**
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
**/

我们再来看一个变式,将 async1 中 await 后面的代码和 async2 的代码都改为异步的,代码如下:

async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

/**
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
**/

我们再来看一个变式,代码如下:

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')


/**
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
**/

参考资料:

  • “Event loops”, section in HTML5 spec.
  • “Help, I’m stuck in an event-loop” by Philip Roberts (video).

文章转自:https://limeii.github.io/2019/05/js-eventloop/ 作者:Li Mei

前端达人公众号.jpg 欢迎关注「前端达人」公众号


Tags

javascriptbasic
Previous Article
如何在开发环境中用node代理解决前后端联调跨域问题
前端达人

前端达人

专注前端知识分享

Table Of Contents

1
engine runtime 和 call stack 简介
2
Event Loop 运行机制的详解
3
microtasks 和 macrotask 的执行顺序

相关文章

「短文」如何在 JavaScript 中随机数组
November 03, 2022
1 min

前端站点

VUE官网React官网TypeScript官网

公众号:前端达人

前端达人公众号