浏览器中的事件循环

1. 浏览器进程和线程

浏览器是多进程的,每当你打开一个 Tab ,都会有一个进程被系统创建出来。 这就是为什么你

会在任务管理器中看见多个 chrome.exe 的原因。

每一个 tab 都是一个浏览器『渲染进程』。 当然,除了一个( 或多个 )渲染进程,浏览器还有其它

的进程,例如 GPU 进程等。

渲染进程是浏览器最核心最重要的进程,而渲染进程下又有多个线程,其中与 JS 关系最大的

就是「JS 引擎线程」。

我们常说的「浏览器中的 JS 是单线程执行的」指的就是浏览器 ( 的一个 Tab 中 ) 有且仅有一

个线程执行你所编写的 JS 代码。

复制代码
1.  浏览器
2.  ├── 其它进程
3.  ├── ...
4.  ├── ...
5.  ├── 渲染进程 1 // 一个 tab 一个进程
6.  │ ├── JS 引擎线程
7.  │ ├── HTTP 请求线程
8.  │ ├── 定时触发线程
9.  │ ├── 事件触发线程
10.  │ └── GUI 线程
11.  ├── 渲染进程 2 // 一个 tab 一个进程
12.  │ ├── JS 引擎线程
13.  │ ├── HTTP 请求线程
14.  │ ├── 定时触发线程
15.  │ ├── 事件触发线程
16.  │ └── GUI 线程
17.  ├── ...
18.  └── 渲染进程 N // 一个 tab 一个进程
19.  ├── JS 引擎线程
20.  ├── HTTP 请求线程
21.  ├── 定时触发线程
22.  ├── 事件触发线程
23.  └── GUI 线程

JS 引擎线程也被称作『主线程』。

2. 事件循环和执行栈

JS 引擎线程在解析一段 JS 代码时,会将代码放在某个地方,这个地方就被称作『执行栈』,

然后依次执行里面的函数 ( 代码 ) 。

JS 引擎线程 ( 即,主线程 ) 会执行 JS 代码,但并非所有的 JS 代码都是由 JS 引擎线程 ( 即,主线程 ) 执行!

当在依次执行这些代码过程中,如果遇到了异步代码 ( 例如,发起 ajax 请求、设置定时任务等

) 时,JS 引擎线程就会将这些代码交给其它线程执行 ( 而不是自己亲自、立刻执行它们 ) 。当 JS引擎线程把异步任务代码交给别人执行之后,执行栈中的执行流程就继续向下执行 ( 而不是在

异步任务代码处干等、白耗时间 ) 。

当执行栈中的同步代码执行完之后 ( 异步代码都交出去了 ) ,JS 引擎线程会从「一个队列」中

去取出已完成的异步任务的回调,将这个 ( 这些 ) 回调函数放到执行栈中继续执行。如果回调

中又有异步代码,这些异步代码还是会被 JS 引擎线程「交出去」。

例如前面例子中:

「发起 ajax 请求」代码会被交给「HTTP 请求线程」执行;

「设置定时任务」代码会被交给「定时触发线程」执行。

==这些异步任务代码,都不是由 JS 引擎线程执行的!==

被交给「其它线程」执行的异步任务在被「其它线程」执行完成后,它们 ( 异步任务 ) 的回调

函数会被放入『任务队列 ( 即,上面所说的「一个队列」 ) 』,而 JS 引擎线程在执行完同步代码之后,会从「任务队列」中取出这些已完成的异步任务的回调继续执行。

上述工作,JS 引擎线程是『周期性、循环』执行的!这个周期性的循环也被称作事件循环,

每一次循环其实就是一个时间周期,也被称之为一次 tick 。

3. 宏任务和微任务

上面所说的「任务队列」不止一个。根据任务种类的不同可以分为「微任务队列」和「宏任务

队列」。

在事件循环的过程中,执行栈在同步代码执行完成之后,优先会去检查「微任务栈」中是否有

任务 ( 代码 ) 需要执行,如果没有,再去「宏任务队列」中查看有没有需要执行的任务。如此

往复。口诀:同微宏。

微任务会比宏任务先执行,并且微任务只有一个队列。宏任务队列可能会有多个。

前面举例的 ajax 请求任务和定时器任务都是宏任务,另外,鼠标点击、键盘按下等事件也属于宏任务。

常见的微任务有 promise.then()、promise.catch()、new MutationObserver()、process.nextTick()等。

补充:从某种意义上讲,微任务不能算作严格意义上的异步任务。因为在底层实现上,V8 引

擎并不会将属于微任务的异步任务交给其它线程执行,仍然是由 JS 引擎线程执行:将它的回

调直接在执行完同步代码之后立刻直接执行。逻辑感官上,微任务好似改变了执行顺序的同步

代码。

宏任务特征:有明确的异步任务需要执行和回调,需要其它线程支持;

微任务特征:没有明确地任务需要执行,只有回调,不需要其它线程支持。

javascript 复制代码
1.  console.log('同步代码1');
2.  setTimeout(() => {
3.      console.log('setTimeout');
4.  }, 0); // 这里的 0 ,实际上是默认 4 毫秒,它不是真正的 0
5.  new Promise((resolve) => {
6.      console.log('同步代码2');
7.      resolve();
8.  }).then(() => {
9.      console.log('promise.then');
10.  });
11.  console.log('同步代码3');
  • setTimeout 和 promise.then 都是异步执行的,将在所以的同步代码块之后执行;
  • promise.then 是微任务,而 setTimeout 是宏任务,所以,promise.then 先执行;
  • new promise 是同步执行的

4. 案例

案例一:在主线程上添加宏任务

javascript 复制代码
1.  // 执行结果:1 3 2
2.
3.  console.log(1);
4.
5.      setTimeout(function () {
6.      console.log(2);
7.  }, 0);
8.
9.  console.log(3);

上述代码 3 条语句:

  • 一个打印
  • 一个定时器
  • 一个打印

先执行第一个打印,输出 1 ,然后由于 setTimeout 是异步操作,因此它将被「其它线程」执

行,而它的回调 console.log(2) 会被放入宏队列。接着执行第二个打印,输出 3 ,最后主

线程再取出宏队列中的代码,执行 console.log(2) ,输出 2 。

所以最终的执行结果是 1 3 2 。

案例二:在主线程上添加微任务

javascript 复制代码
1.  // 最终执行结果:1 2 4 3
2.
3.  console.log(1);
4.  new Promise(function(resolve,reject) {
5.      console.log('2');
6.      resolve();
7.  }).then(function() {
8.      console.log(3);
9.  })
10.  console.log(4);

上述代码 4 条语句:

  • 一个打印
  • 一个 new promise
  • 一个 promise.then
  • 一个打印

先执行第一个打印,输出 1 ,然后执行 new Promise ,由于 new Promise 是同步代码,它的参数函数代码会立即执行,所以会执行 console.log(2) 输出 2 。而 promise.then 作为Promise 的回调,会被放入微队列。紧接着执行 console.log(4) 输出 4 ,最后主线程再取出微队列中的 console.log(3) 执行,输出 3 。

所以最终的执行结果是 1 2 4 3。

案例三:宏任务中创建微任务

javascript 复制代码
1.  // 最终结果:1 5 11 6 2 3 4 7 8 10 9
2.  console.log('1');
3.
4.  setTimeout(function () {
5.      console.log('2');
6.      new Promise(function (resolve) {
7.      console.log('3');
8.      resolve();
9.  }).then(function () {
10.      console.log('4')
11.  })
12.  }, 0);
13.
14.  new Promise(function (resolve) {
15.      console.log('5');
16.      resolve();
17.  }).then(function () {
18.      console.log('6')
19.  });
20.
21.  setTimeout(function () {
22.      console.log('7');
23.      new Promise(function (resolve) {
24.      console.log('8');
25.      resolve();
26.  }).then(function () {
27.      console.log('9')
28.  })
29.  console.log('10')
30.  }, 0);
31.
32.  console.log('11');
  • 首先会执行第一个打印,输出 1 ;
  • 然后第 1 个 setTimeout 定时任务会被「其它线程」执行,其回调( new Promise() {}

)会被放入宏队列,而主线程继续向下;

  • 接着执行第 1 个 new Promise 其代码会立即执行,因此会输出 5 。而它的 .then() 方法

的回调( console.log(6) )将会被放入微队列;

  • 主线程继续向下,遇到第 2 个 setTimeout 定时任务,它又会被「其它线程」执行,其回

调( new Promise() {} )会被放入宏队列,而主线程继续向下;

  • 主线程执行 console.log(11) 输出 11 。截止目前为止,输出的是 1 5 11 。然后,主线

程的代码全部执行完,主线程开始从微队列和宏队列中取代码执行。

  • 现在微队列中有且仅有的代码是输出 5 的 new Promise 的回调: console.log(6) 。它

被主线程取出,执行。

  • 微队列当前被清空了,主线程从宏队列中取代码执行。首先取出的是第 1 个定时任务的回

调,先输出 2 ,然后执行 new Promise 输出 3 ,然后 promise.then 的回调

console.log(4) 被放入微队列。

  • 当前主线程代码又执行结束,然后微队列中有了一条语句,被取出执行,输出 4 。
  • 主线程代码队列和微队列全空,主线从又从宏队列中取代码,执行第 2 个的回调,打印 7

和 8 ,将 promise.then 的回调 console.log(9) 扔进微队列后,继续打印 10 。

  • 最后,主线程从微队列中取出 console.log(9) 执行,输出 9 。

案例四:微任务中创建宏任务

javascript 复制代码
1.  // 1 5 2 3 4
2.  new Promise((resolve) => {
3.      console.log("1")
4.      resolve()
5.  }).then(() => {
6.      console.log("2")
7.      setTimeout(() => {
8.          console.log("3")
9.      }, 0)
10.  })
11.
12.  setTimeout(() => {
13.      console.log("4")
14.  },1000);
15.
16.  console.log("5")
  • 首先 new Promise 执行,执行 console.log(1) 输出 1 ,而后 promise.then 被仍进微

队列。

  • 紧接着下面的定时任务代码执行,其回调,会在 4 秒后被「其它线程」扔进宏队列。主线

程继续向下。

  • 主线程执行 console.log(5) 输出 5 ,至此,主线程代码队列中全部执行完,主线程开始

从为队列中拿代码执行。

  • 主线程取出 promise.then 的回调输出 2 ,然后遇到定时器,由于上面定时器间隔时间更

短,因此上面定时器的回调 console.log(3) 会比下面的定时器的回调 console.log(5)

先进入宏队列,因此,主线程先取到、执行的是输出 3 ,后执行的是输出 4 。

相关推荐
腾讯TNTWeb前端团队35 分钟前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom6 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试