JavaScript运行机制

本文最后更新于 2025年7月30日 下午

参考:
https://juejin.cn/post/6844903512845860872
https://juejin.cn/post/6844904100195205133
https://juejin.cn/post/6991849728493256741
https://juejin.cn/post/7126795602276384804

JavaScript 异步实现

JavaScript 是一门单线程语言,虽然 HTML5 中提出 Web-Worker,但 JavaScript 是单线程这一核心仍未改变。所以一切 JavaScript 的”多线程”都是用单线程模拟出来的。

JavaScript 的主运行线程只有一个,但不是整个运行环境都是单线程。JavaScript 的运行环境主要是浏览器,它不仅是多线程的,而且是多进程的。

浏览器的多进程架构

以 Chrome 浏览器为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合来完成浏览器的整体功能。每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。

Chrome 浏览器采用多进程架构,其顶层存在一个 Browser 进程用以协调浏览器的其它进程。

Chrome 浏览器的主要进程

上图只是一个概括,意思是浏览器有这几类的进程和线程,并不是每种只有一个,比如渲染进程就有多个,每个页面都有自己的渲染进程。有时候使用浏览器会遇到某个页面崩溃或者没有响应的情况,这个页面对应的渲染进程可能崩溃了,但是其它页面并没有用这个渲染进程,它们各自有各自的渲染进程,所以其它页面并不会受影响。

最新 Chrome 浏览器包括:1 个 Browser 进程、1 个 GPU 进程、1 个网络进程、多个渲染进程、多个插件进程。打开浏览器的一个页面至少需要 4 个进程:1 个 Browser 进程、1 个 GPU 进程、1 个网络进程、1 个渲染进程。

相较于单进程而言,多进程的优势:

  • 避免单个页面奔溃影响整个浏览器
  • 避免第三方插件奔溃影响整个浏览器
  • 多进程可以充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器的稳定性

Browser 进程

浏览器的主进程(只有一个),负责协调、主控:

  • 负责浏览器的界面显示与用户交互,包括地址栏、书签、前进后退按钮等。
  • 负责各个页面的创建和销毁。
  • 负责与其他进程的协调工作。
  • 文件存储等功能。

Browser 进程中的线程:

  • UI 线程:该线程是程序运行的主线程,是程序的入口点,用来处理用户交互(如监听用户输入、前进后退等)。同时,会分发任务给其他相应线程去执行。
  • IO 线程:处理 Browser 进程与其他进程进行进程间通信,下载 Renderer 进程所需的资源文件等。
  • File 线程:读取磁盘文件,下载文件到磁盘等。
  • 数据库线程:进行一些数据库操作,例如保存 Cookie 到数据库。
  • 历史记录线程,http 服务代理线程,等等。

Browser 进程与 Renderer 进程间的通信(以开启一个新 tab 为例):

  • Browser 进程中的 UI 线程 处理用户交互,接收到用户请求,转交给 IO 线程
  • Browser 进程中的 IO 线程 获取页面内容(通过网络请求或本地缓存),随后将该任务通过 RendererHost 接口传递给 Renderer 进程
  • Renderer 进程 的 Renderer 接口 收到消息,简单解释后交给渲染线程,进行 html 和 css 解析,渲染页面,js 执行等任务
  • Renderer 进程将得到的结果传递给 Browser 进程
  • Browser 进程 接收到结果并在界面上绘制出图像

GPU 进程

负责整个浏览器界面的渲染。Chrome 刚开始发布的时候是没有 GPU 进程的,而使用 GPU 的初衷是为了实现 3D CSS 效果,只是后面网页、Chrome 的 UI 界面都用 GPU 来绘制,这使 GPU 成为浏览器普遍的需求,最后 Chrome 在多进程架构上也引入了 GPU 进程。

网络进程

负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面,后面才独立出来成为一个单独的进程。

音频进程

浏览器的音频管理。

Plugin 进程

负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响。每种类型的插件对应一个进程,仅当使用插件时才创建插件进程。

Renderer 进程

负责控制显示标签页内的所有内容,核心任务是将 HTML、CSS、JS 转为用户可以与之交互的网页,排版引擎 Blink 和 JS V8 引擎都是运行在该进程中,默认情况下 Chrome 会为每个标签页创建一个渲染进程。渲染进程被称为浏览器渲染进程或浏览器内核,其内部是多线程的。

浏览器的渲染进程

我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成,对于前端工程师来说,主要关心的还是渲染进程。

GUI 渲染线程

GUI 渲染线程负责渲染浏览器界面,解析 HTML 和 CSS,构建 DOM Tree、CSSOM Tree、Render Tree,布局和绘制页面。

触发条件:当界面需要重绘(repaint)或由于某种操作引发回流(reflow)时,线程就会执行。

注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎线程执行时 GUI 线程会被挂起(相当于被冻结了),GUI 渲染线程会被保存在一个队列中,等到 JS 引擎线程空闲时再立即被执行。

JS 引擎线程

JS 引擎线程是负责执行 JavaScript 的主线程,“JavaScript 是单线程的”就是指的这个线程。Chrome V8 引擎就是在这个线程运行的。JS 引擎线程负责解析 Javascript 脚本,运行代码。一个页面只有一个 JS 引擎线程负责解析和执行 Javascript。

需要注意的是,这个线程跟 GUI 渲染线程是互斥的。互斥的原因是 JavaScript 也可以操作 DOM 和 CSSOM,如果 JS 引擎线程和 GUI 渲染线程同时操作,结果就混乱了,不知道到底渲染哪个结果。互斥带来的后果就是如果 JavaScript 长时间运行,GUI 渲染线程就不能执行,造成页面的渲染不连贯,导致页面渲染加载阻塞,整个页面就感觉卡死了。

定时触发器线程

setTimeout 和 setInterval 就运行在这里,它跟 JS 引擎线程不在同一个地方,因为 JS 引擎线程是单线程的,所以如果其处于阻塞状态,那么计时器就会不准确,所以需要单独的线程来负责计时器工作,因而“单线程的 JavaScrip”能够实现异步。

定时触发器线程其实只是一个计时的作用,它并不会真正执行时间到了的回调函数,真正执行这个回调函数的还是 JS 引擎线程。所以当时间到了,定时触发器线程会将回调函数给到事件触发线程,然后事件触发线程将它加到事件队列里面去。最终 JS 引擎线程从事件队列取出这个回调函数执行。

HTTP 请求线程

HTTP 请求线程负责处理异步的 Ajax 请求。当一个请求完成后,如果设置有回调函数,它就会通知事件触发线程,然后事件触发线程将这个事件放入事件队列给 JS 引擎线程执行。

事件触发线程

事件触发线程主要用来控制事件循环,比如 JavaScript 执行遇到定时器,AJAX 异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到事件队列,等待 JS 引擎线程来处理。事件触发线程不仅会将定时器事件放入事件队列,其它满足条件的事件也是它负责放进事件队列。

JavaScript 异步的实现靠的就是浏览器的多线程,当主线程遇到异步任务时,就将这个任务交给对应的线程,当这个异步任务满足回调函数条件时,对应的线程又通过事件触发线程将这个任务放入事件队列,然后主线程从事件队列取出事件继续执行。事件队列(Event Queue)是事件循环(Event Loop)的一部分。

Event Loop

Event Loop 是 JavaScript 管理事件执行的一个流程,具体的管理办法由它具体的运行环境确定。
JavaScript 的主要运行环境有两个:浏览器和 Node.js。这两个环境的 Event Loop 有区别。

宏任务和微任务

Event Queue 里面的事件分两类:宏任务和微任务。

微任务拥有更高的优先级,Event Loop 遍历队列时会先检查微任务队列,如果里面有任务就全部拿来执行,执行完所有微任务之后再执行下一个宏任务。执行每个宏任务之前都检查微任务队列是否有任务,如果有会优先执行微任务队列。

常见宏任务 浏览器环境 Node.js 环境
I/O
script(外层同步代码)
setTimeout
setInterval
setImmediate
requestAnimationFrame
常见微任务 浏览器环境 Node.js 环境
Promise
async/await
process.nextTick
MutaionObserver

不同类型的任务会进入对应的 Event Queue,比如 setTimeout 和 setInterval 会进入相同的 Event Queue。

事件循环的顺序,决定 JS 代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个事件队列执行完毕,再执行所有的微任务。

浏览器的 Event Loop

Event Loop 是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是 Event Queue。各个异步线程执行完后,通过事件触发线程将回调函数放到 Event Queue。

浏览器的 Event Loop

执行流程:

  • 主线程执行时,先看要执行的是同步任务还是异步任务;
  • 同步任务进入主线程,异步任务进入相对应的线程,主线程继续执行同步任务;
  • 相对应的线程执行异步任务,满足条件后对应的线程将异步任务的回调函数放入 Event Queue;
  • 主线程内的任务执行完毕后就去 Event Queue 取出里面的函数进入主线程执行;

主线程不断循环上述执行流程,就是浏览器的 Event Loop。

计时不准

Event Loop 的这个流程里面其实还是隐藏了一些问题的,最典型的就是总是先执行同步任务,然后再执行 Event Queue 里面的回调函数。这个特性就直接影响了定时器的执行。如果主线程长时间被阻塞,定时器的回调函数就没机会执行,即使执行了时间也不准。所以写代码时一定不要长时间占用主线程。

setTimeout

setTimeout() 用于指定在一定时间后执行某些代码。实际中,是指经过指定时间后,把要执行的任务加入到 Event Queue 中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间大于指定的时间。

setTimeout(fn, 0) 含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少时间,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。但是,即便主线程为空,0 毫秒实际上也达不到。根据 HTML 的标准最低是 4 毫秒。而且,setTimeout(fn, 0) 会被强制改为 setTimeout(fn, 1)。

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log("执行啦1");
}, 1000);
setTimeout(() => {
console.log("执行啦2");
}, 0);
console.log("先执行这里");
// 先执行这里
// 执行啦2
// 执行啦1
1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log("执行啦1");
}, 1);
setTimeout(() => {
console.log("执行啦2");
}, 0);
console.log("先执行这里");
// 先执行这里
// 执行啦1
// 执行啦2

setInterval

setInterval() 用于指定每隔一段时间执行某些代码,直到取消循环或者页面卸载。setInterval() 会在每过指定的时间,将注册的函数放入 Event Queue。同理,如果 Event Queue 前面的任务耗时太久,一样需要等待。

唯一需要注意的一点是,对 setInterval(fn, ms) 来说,我们已经知道不是每过 ms 秒会执行一次 fn,而是每过 ms 秒,会有 fn 进入 Event Queue。一旦 setInterval 的回调函数 fn 执行时间超过了延迟时间 ms,那就完全看不出来有指定的时间间隔。

Node.js 的 Event Loop

Node.js 是运行在服务端的 JavaScrip,虽然它也用到了 V8 引擎,但是它的服务目的和环境不同,导致了它的 API 与原生 JavaScript 有些区别,它的 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以与浏览器 Event Loop 也是不一样的。Node.js 的 Event Loop 是分阶段的,而且 Node.js 是先将外层的同步代码一次性全部执行完,遇到异步任务就塞到对应的阶段,然后进入 Event Queue 进行阶段循环。

Node.js 的 Event Loop

不同阶段:

  • timers:执行 setTimeout 和 setInterval 的回调函数;
  • pending callbacks:执行延迟到下一个循环迭代的 I/O 回调函数;
  • idle:仅系统内部使用;
  • prepare:仅系统内部使用;
  • poll:检索新的 I/O 事件、执行与 I/O 相关的回调函数;
  • check:setImmediate 在这里执行;
  • close callbacks:一些关闭的回调函数。

不断循环上述执行阶段,就是 Node.js 的 Event Loop。

每个阶段都有一个自己先进先出的队列,只有当这个队列的事件执行完或者达到该阶段的上限时,才会进入下一个阶段。在每次事件循环之间,Node.js 都会检查它是否在等待任何一个 I/O 或者定时器,如果没有程序就关闭退出。我们的直观感受就是,如果一个 Node.js 程序只有同步代码,在控制台运行完后它就退出了。

需要注意的是 poll 阶段,它后面并不一定每次都是 check 阶段,poll 队列执行完后,如果没有 setImmediate 但是有定时器到期,它会绕回去执行定时器阶段。

setImmediate

在一个异步流程里,setImmediate 会比定时器先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 Node.js 执行
console.log("outer");
setTimeout(() => {
setTimeout(() => {
console.log("setTimeout");
}, 0); // setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)
setImmediate(() => {
console.log("setImmediate");
});
}, 0);
// outer
// setImmediate
// setTimeout

上面console.log('setTimeout')console.log('setImmediate')都包在了一个 setTimeout 里面,如果直接写在最外层会怎么样呢?上面没有问题,但是,下面的代码输出结果不一定。

1
2
3
4
5
6
7
8
9
10
11
// 在 Node.js 执行
console.log("outer");
setTimeout(() => {
console.log("setTimeout");
}, 0); // setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)
setImmediate(() => {
console.log("setImmediate");
});
// outer
// setImmediate
// setTimeout
1
2
3
4
5
6
7
8
9
10
11
// 在 Node.js 执行
console.log("outer");
setTimeout(() => {
console.log("setTimeout");
}, 0); // setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)
setImmediate(() => {
console.log("setImmediate");
});
// outer
// setTimeout
// setImmediate

分析执行流程:

  • 外层同步代码一次性全部执行完,遇到异步 API 就塞到对应的阶段
  • 遇到 setTimeout,虽然设置是 0 毫秒触发,但是被 node.js 强制改为 1 毫秒,塞入 times 阶段
  • 遇到 setImmediate 塞入 check 阶段
  • 同步代码执行完毕,进入 Event Loop
  • 先进入 times 阶段,检查是否过去 1 毫秒:若已过,满足 setTimeout 条件,执行回调;若未过,跳过
  • 跳过空的阶段,进入 check 阶段,执行 setImmediate 回调

关键就在这个 1 毫秒,如果同步代码执行时间较长,进入 Event Loop 的时候 1 毫秒已经过了,setTimeout 执行,如果 1 毫秒还没到,就先执行了 setImmediate。每次我们运行脚本时,机器的状态可能不一样,导致运行时有 1 毫秒的差距,一会儿 setTimeout 先执行,一会儿 setImmediate 先执行。但是这种情况只会发生在还没进入 timers 阶段的时候。像第一个例子那样,因为已经在 timers 阶段,所以里面的 setTimeout 只能等下个循环,所以 setImmediate 肯定先执行。同理的还有其他 poll 阶段也是这样。

process.nextTick

process.nextTick() 是一个特殊的异步任务,它不属于任何的 Event Loop 阶段。实际 Node 在遇到这个 API 时 Event Loop 根本就不会继续进行,而是马上停下来执行 process.nextTick(),执行完后继续 Event Loop。可以理解为,该任务执行在同步任务执行之后、异步任务执行之前;或者理解为,该任务在微任务队列最前列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在 Node.js 执行
var fs = require("fs");
fs.readFile(__filename, () => {
console.log("start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
process.nextTick(() => {
console.log("nextTick 2");
});
});
process.nextTick(() => {
console.log("nextTick 1");
});
console.log("end");
});
// start
// end
// nextTick 1
// setImmediate
// nextTick 2
// setTimeout

分析执行流程:

  • 代码基本都在 readFile 回调里面,它自己执行时,已经在 poll 阶段
  • 遇到 setTimeout(fn, 0),其实是 setTimeout(fn, 1),塞入之后的 timers 阶段
  • 遇到 setImmediate 塞入 check 阶段
  • 遇到 nextTick 立马执行,输出nextTick 1
  • 到 check 阶段,输出setImmediate,又遇到 nextTick 立马执行,输出nextTick 2
  • 到 timers 阶段,输出setTimeout

这种机制其实类似于微任务,但是并不完全一样,同时有 nextTick 和 Promise 时是 nextTick 先执行,原因是 nextTick 的队列比 Promise 队列优先级更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在 Node.js 执行
const promise = Promise.resolve();
setImmediate(() => {
console.log("setImmediate");
});
promise.then(() => {
console.log("promise");
});
process.nextTick(() => {
console.log("nextTick");
});
console.log("fafa");
// fafa
// nextTick
// promise
// setImmediate

实践分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
console.log(0);

function test(params) {
console.log(1);
return new Promise((resolve, reject) => {
if (params > 0) {
console.log(2);
resolve(params);
console.log(3);
} else {
console.log(4);
reject(params);
console.log(5);
}
});
}
function tryTest(x) {
test(x)
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(-err);
});
}

console.log(6);
tryTest(100);
console.log(7);
tryTest(-200);
console.log(8);
// 0
// 6
// 1
// 2
// 3
// 7
// 1
// 4
// 5
// 8
// 100
// 200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
console.log(0);

function test(params) {
return new Promise((resolve, reject) => {
if (params > 0) {
console.log(1);
resolve(params);
console.log(2);
} else {
console.log(3);
reject(params);
console.log(4);
}
});
}
async function tryTest(x) {
console.log("x1:", x);
const res = await test(x);
console.log("x2:", x);
console.log(res);
}

console.log(5);
tryTest(100);
console.log(6);
tryTest(-200);
console.log(7);
// 0
// 5
// x1:100
// 1
// 2
// 6
// x1:-200
// 3
// 4
// 7
// x2:100
// 100
// Uncaught (in promise) -200
1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function () {
console.log("setTimeout");
});

const promise = new Promise(function (resolve) {
console.log("promise");
}).then(function () {
console.log("then");
});
console.log("console");
// promise
// console
// setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = 0;
async function printa() {
console.log("1:", a);
let result = a + (await 10);
console.log("2:", a);
console.log(result);
}
printa();
console.log("log");
a++;
console.log("3:", a);
// 1:0
// log
// 3:1
// 2:1
// 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var b = 0;
async function printb() {
console.log("1:", b);
let result = (await 10) + b;
console.log("2:", b);
console.log(result);
}
printb();
console.log("log");
b++;
console.log("3:", b);
// 1:0
// log
// 3:1
// 2:1
// 11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
async function async1() {
console.log("async1 start");
await async2().then(() => {
console.log("async2 end");
});
console.log("async1 end");
}

async function async2() {
console.log("async2 start");
}

console.log("script start");

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

async1();

new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise2");
}).then(function () {
console.log("promise3");
});
// script start
// async1 start
// async2 start
// promise1
// promise2
// async2 end
// promise3
// async1 end
// setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async function async1() {
console.log("async1 start");
await async3();
await async2().then(() => {
console.log("async2 end");
});
console.log("async1 end");
}

async function async2() {
console.log("async2 start");
}

async function async3() {
console.log("async3");
}

console.log("script start");

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

async1();

new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise2");
}).then(function () {
console.log("promise3");
});
// script start
// async1 start
// async3
// promise1
// promise2
// async2 start
// promise3
// async2 end
// async1 end
// setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 在 Node.js 执行
console.log("1");

setTimeout(function () {
console.log("2");
process.nextTick(function () {
console.log("3");
});
new Promise(function (resolve) {
console.log("4");
resolve();
}).then(function () {
console.log("5");
});
});

new Promise(function (resolve) {
console.log("6");
resolve();
}).then(function () {
console.log("7");
});

process.nextTick(function () {
console.log("8");
});

setTimeout(function () {
console.log("9");
process.nextTick(function () {
console.log("10");
});
new Promise(function (resolve) {
console.log("11");
resolve();
}).then(function () {
console.log("12");
});
});
// 1 6 8 7 2 4 3 5 9 11 10 12

JavaScript运行机制
https://xuekeven.github.io/2021/08/20/JavaScript运行机制/
作者
Keven
发布于
2021年8月20日
更新于
2025年7月30日
许可协议