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
相关推荐
zqx_718 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己35 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼2 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发