JavaScript执行机制
JavaScript 的执行机制涉及到几个关键的概念,包括单线程执行、事件循环、调用栈、任务队列和异步操作。
关键词:
- JavaScript 是一门单线程的编程语言,这意味着它只有一个主执行线程来处理所有的任务
- JavaScript 可以利用异步编程 的方式实现并发操作,从而提高性能和用户体验
- JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务
- 进程、线程:⚠️补充资料,方便理解
- 同步、异步
- 宏任务、微任务
说起JavaScript执行机制,比较常谈事件循环所以先介绍事件循环 Event Loop
事件循环 Event Loop
概念:
- 事件循环是 JavaScript 中处理异步操作的机制。
- 虽然 JavaScript 是单线程的,但通过事件循环机制,可以实现非阻塞的异步操作。
- 事件循环是 JavaScript 运行时环境中的一部分,负责管理调用栈、任务队列(Task Queue)等。它确保 JavaScript 单线程执行模型下的异步任务能够按照特定的顺序执行。
之所以称之为 事件循环,是因为它通常按如下方式实现:
javascript
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage()
会同步地等待消息到达 (如果当前没有任何消息等待被处理)。
事件循环基本的执行流程如下:
-
进入 script 标签开始第一个事件循环
-
所有同步代码在主线程上执行,将函数调用推入调用栈。
-
遇到异步操作时,将其回调函数注册到主线程之外的任务队列(task queque),继续执行同步任务。
- 遇到宏任务(例如,setTimeout,XMLHttpRequest),放入宏任务队列
- 遇到微任务(例如,Promise 回调),放入微任务队列
-
当调用栈为空时,事件循环检查任务队列,如果有,将任务取出并压入调用栈,执行该任务的回调函数。
- 执行微任务队列中的所有微任务
- 清空微任务队列
-
寻找下一个宏任务
-
循环执行上述步骤,保持事件循环一直运行,处理同步和异步任务,直到清空所有宏任务。
这种机制确保了 JavaScript 在处理异步操作时不会阻塞主线程,保持了响应性。
进程与线程
- 进程(Process): 在操作系统中运行的一个程序实例,拥有独立的内存空间和执行环境。进程之间相互独立,不会互相干扰。
- 线程(Thread): 是进程 中更小的执行单位,是由进程 创建和管理的。一个进程可以包含多个线程,它们共享进程的内存空间和其他资源,但拥有独立的执行栈和寄存器。在浏览器中,不同线程协同工作,处理渲染、网络请求和 JavaScript 执行等任务。
- **Chrome 打开一个页面有多少进程:**浏览器从关闭到启动,然后新开一个页面至少需要:1个浏览器进程,1个GPU进程,1个网络进程,和1个渲染进程,一共4个进程。
- 渲染进程:默认情况下会为每一个标签页 配置一个渲染进程。我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成。
- 浏览器tab页渲染进程中的线程协同:
- 在浏览器中每个tab页通常对应一个独立渲染进程,以提高安全性和稳定性。
- 这个进程中,又可以有多个线程来并行处理不同的任务。
- 不同线程之间协同工作,但GUI渲染线程 和JS引擎线程是互斥的,以避免并发访问 DOM 树和样式表引起的冲突。
Chrome 打开一个页面有多少进程:
-
浏览器从关闭到启动,然后新开一个页面至少需要:1个浏览器进程,1个GPU进程,1个网络进程,和1个渲染进程,一共4个进程;
-
后续如果再打开新的标签页:浏览器进程,GPU进程,网络进程是共享的,不会重新启动,然后默认情况下会为每一个标签页 配置一个渲染进程;
-
但是也有
例外
,比如从A页面里面打开一个新的页面B页面,而A页面和B页面又属于同一站点的话,A和B就共用一个渲染进程,其他情况就为B创建一个新的渲染进程 -
最新的Chrome浏览器包括:
1个浏览器主进程
,1个GPU进程
,1个网络进程
,多个渲染进程
,和多个插件进程
-
浏览器进程
: 负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能 -
GPU进程
:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程 -
网络进程
:负责发起和接受网络请求,以前是作为模块运行在浏览器进程一时在面的,后面才独立出来,成为一个单独的进程 -
插件进程
:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响 -
渲染进程
:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎Blink和JS引擎V8都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程
-
渲染进程中的线程:
我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成,所以我们来看一下渲染进程中的线程
GUI渲染线程
:负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行JS引擎线程
:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。它GUI渲染进程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧事件触发线程
:主要用来控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理计时器线程
:指setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作异步http请求线程
: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行
单线程的JavaScript
由于 JavaScript 是单线程的
,它在执行时只能按照顺序逐条执行代码。
JS中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具备并行任务处理的特性,我们称之为"单线程"。
为什么JavaScript是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
这样做有两个优点:
- 节约内存开销: 单线程执行的优势之一是在运行时只需要一个线程来逐行执行代码,不需要为每个线程分配独立的内存空间。这使得 JavaScript 在资源消耗上相对较轻,尤其是对于前端开发中的浏览器环境,能够更高效地利用有限的内存。
- 没有锁的概念: 在多线程编程中,多个线程可能同时访问共享资源,为了保证数据的一致性,需要引入锁机制。然而,锁机制会增加上下文切换的开销,可能导致性能下降。在 JavaScript 的单线程模型中,由于不存在多线程同时访问的情况,避免了引入锁的复杂性和相关的开销,简化了代码的编写和维护。
同步异步编程
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
- 回调函数
callback
Promise/async
await
Generator
- 事件监听
- 发布/订阅
- 计时器
requestAnimationFrame
MutationObserver
process.nextTick
- I/O操作
任务队列:宏任务与微任务
JavaScript 通过异步编程的方式来实现并发操作
。它将异步任务分为宏任务和微任务两种类型。
宏任务(Macrotask)
包括以下几种:
- script:整体的 JavaScript 代码块。
- setTimeout 和 setInterval:定时器任务。
- setImmediate:在当前事件循环完成后立即执行的任务。
- I/O 操作:例如网络请求、文件读写等。
- UI 渲染:浏览器需要绘制页面时触发的任务。
微任务(Microtask)
包括以下几种:
- Promise.then():Promise 的回调函数。
- MutationObserver:DOM 变动观察器。
- process.nextTick():Node.js 中的微任务。
待补充(也可直接mdn)
- setTimeout
- setInterval
- Promise与process.nextTick(callback)
- async/await
代码测试
例题1
javascript
async function async1() {
console.log('async1 start'); // 主4
await async2(); // 主5
console.log('async1 end'); // 微1
}
async function async2() {
console.log('async2'); // 主6
}
console.log('script start'); // 主1
setTimeout(function () { // 主2
console.log('setTimeout'); // 队1
}, 0)
async1(); // 主3
new Promise(function (resolve) { // 主7
console.log('promise1'); // 主8
resolve(); // 微2
}).then(function () { // 主9
console.log('promise2'); // 微3
});
console.log('script end'); // 主10
// 按 主1-10,微1-3,队1 执行顺序,输出如下
// script start -> 主1
// async1 start -> 主4
// async2 -> 主6
// promise1 -> 主8
// script end -> 主10
// async1 end -> 微1
// promise2 -> 微3
// setTimeout -> 队1