JavaScript执行机制

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 在处理异步操作时不会阻塞主线程,保持了响应性。

进程与线程

  1. 进程(Process): 在操作系统中运行的一个程序实例,拥有独立的内存空间和执行环境。进程之间相互独立,不会互相干扰。
  2. 线程(Thread):进程 中更小的执行单位,是由进程 创建和管理的。一个进程可以包含多个线程,它们共享进程的内存空间和其他资源,但拥有独立的执行栈和寄存器。在浏览器中,不同线程协同工作,处理渲染、网络请求和 JavaScript 执行等任务。
  3. **Chrome 打开一个页面有多少进程:**浏览器从关闭到启动,然后新开一个页面至少需要:1个浏览器进程,1个GPU进程,1个网络进程,和1个渲染进程,一共4个进程。
  4. 渲染进程:默认情况下会为每一个标签页 配置一个渲染进程。我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成。
  5. 浏览器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单线程的本质。

这样做有两个优点:

  1. 节约内存开销: 单线程执行的优势之一是在运行时只需要一个线程来逐行执行代码,不需要为每个线程分配独立的内存空间。这使得 JavaScript 在资源消耗上相对较轻,尤其是对于前端开发中的浏览器环境,能够更高效地利用有限的内存。
  2. 没有锁的概念: 在多线程编程中,多个线程可能同时访问共享资源,为了保证数据的一致性,需要引入锁机制。然而,锁机制会增加上下文切换的开销,可能导致性能下降。在 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
相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼14 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax