深入理解JavaScript运行原理:进程线程、异步与 Event-Loop 机制

200 阅读7分钟

在前端开发的世界里,理解进程、线程、异步操作以及 Event-Loop 机制,是掌握 JavaScript 运行原理的关键。这影响着我们编写的每一行代码的执行效率和行为表现。本文将深入剖析这些核心概念,帮助你揭开前端运行机制的神秘面纱。

一、进程与线程:前端运行的基础单元

进程和线程是操作系统层面的概念,它们为程序的运行提供了基础的执行环境。

1.1 进程

进程可以理解为 CPU 在运行指令和保存上下文所需要的一段时间。以微信为例,从系统执行打开微信的指令开始,到加载微信的上下文环境,直至彻底关闭微信之前的这段时间,都属于一个进程。进程是系统进行资源分配和调度的基本单位,它拥有独立的内存空间和系统资源。

在浏览器中,新开一个页面就相当于开启了一个进程。这个进程承载着页面运行所需的各种资源和任务,它是页面运行的 “容器”。

1.2 线程

线程是进程中的一个更小的单位,指的是执行一段指令所需的时间。在一个进程中,可以包含多个线程,它们共享进程的资源,协同完成复杂的任务。比如在微信中,打开聊天界面需要一个渲染线程来绘制界面元素,获取最新消息则需要网络线程来处理数据请求。

在浏览器页面的进程中,存在着多个重要的线程,它们相互配合,最终将页面展示给用户:

  • http 线程:负责处理网络请求,从服务器获取页面所需的资源,如 HTML、CSS、JavaScript 文件等。

  • js 引擎线程:专门用于解析和执行 JavaScript 代码,是 JavaScript 运行的核心。

  • 渲染线程:负责解析 HTML 和 CSS,构建 DOM 树和 CSSOM 树,并将它们渲染成可视化的页面。

需要特别注意的是,js 引擎线程和渲染线程是互斥的。这意味着当 js 引擎线程在执行 JavaScript 代码时,渲染线程会被阻塞,无法进行页面渲染;反之亦然。这种机制是为了保证页面渲染的一致性和稳定性,但也可能导致一些性能问题,例如长时间执行的 JavaScript 代码会造成页面卡顿。

二、异步:提升 JavaScript 执行效率的关键

JavaScript 是单线程语言,这意味着在同一时间内,它只能执行一个任务。v8 引擎在执行 js 代码时,默认只开启一个线程。如果所有任务都按照顺序同步执行,遇到耗时的操作(如网络请求、文件读取等)时,整个程序就会被阻塞,导致页面失去响应,用户体验极差。

为了解决这个问题,JavaScript 引入了异步操作。当遇到异步代码时,v8 引擎不会阻塞后续代码的执行,而是将异步代码存放到任务队列中,等到 js 引擎的任务空闲时,再从任务队列中取出异步任务进行执行。这样,同步代码和异步代码可以并行执行,大大提高了程序的执行效率和响应性。

三、Event-Loop:异步任务的调度者

JavaScript 代码中有同步和异步之分,而异步任务又被细分为宏任务(macrotask)和微任务(microtask)。

3.1 宏任务与微任务

1. 宏任务(MacroTask)

宏任务是 JavaScript 中粒度较大的异步任务单元,通常与浏览器的事件循环机制直接关联。每个宏任务的执行会形成一个完整的执行栈,且在浏览器的事件循环中,宏任务是按队列顺序逐个执行的。

常见宏任务类型

  • 定时器相关:setTimeoutsetInterval
  • I/O 操作:文件读写、网络请求(如XMLHttpRequest
  • 浏览器渲染操作:UI rendering
  • 消息通信:MessageChannelpostMessage
  • Node.js 中的setImmediate

2. 微任务(MicroTask)

微任务是粒度更小的异步任务,其执行优先级高于宏任务。微任务通常在当前宏任务执行完毕后、下一个宏任务开始前执行,且会在一个批次内将所有微任务全部处理完毕

常见微任务类型

  • Promise 回调:promise.thenpromise.catchpromise.finally
  • MutationObserver:监听 DOM 变化的 API
  • Node.js 中的process.nextTick

3、核心区别对比

对比维度宏任务(MacroTask)微任务(MicroTask)
任务队列数量多个宏任务队列(如定时器队列、I/O 队列等)仅一个微任务队列
执行时机当前宏任务结束后,下一个宏任务开始前当前宏任务内的同步代码执行完毕后立即执行
渲染时机宏任务之间可能触发浏览器渲染微任务执行期间不会触发渲染,需等待微任务队列清空
优先级低于微任务高于宏任务
应用场景处理延迟执行、I/O 操作、页面渲染等粗粒度任务处理 Promise 回调、DOM 变化监听等

3.2 Event-Loop 的执行顺序

Event-Loop 是 JavaScript 实现异步操作的核心机制,它通过不断循环检查调用栈和任务队列,来调度任务的执行。其执行顺序如下:

  1. 执行同步代码:首先执行第一个宏任务中的同步代码,在这个过程中,如果遇到异步任务,则将其存入对应的异步队列(宏任务队列或微任务队列)。

  2. 执行微任务队列:当同步代码执行完毕后,开始执行微任务队列中的任务。如果在执行微任务的过程中又产生了新的微任务,这些新的微任务会被添加到微任务队列的末尾,继续执行,直到微任务队列为空。如果有宏任务则添加到宏任务队列。

  3. 渲染页面:在微任务队列执行完毕后,如果当前存在需要渲染的页面(即有 HTML 内容),浏览器会进行页面渲染,更新页面的可视化内容。

  4. 执行宏任务队列:页面渲染完成后,开始执行宏任务队列中的下一个宏任务,这标志着开启了下一个事件循环周期。如此循环往复,不断处理新的任务。

练习:分析代码执行过程

console.log(1);
new Promise((resolve) => {
  console.log(2);
  resolve();
})
  .then(() => {
    console.log(3);
    setTimeout(() => {
      console.log(4);
    }, 0);
  });

setTimeout(() => {
  console.log(5);
  setTimeout(() => {
    console.log(6);
  }, 0);
}, 0);

console.log(7);

2025-06-30T06_18_03.840Z-454290.gif

四、await:异步编程的语法糖

await是 JavaScript 中用于处理异步操作的一个关键字,它通常与async函数结合使用。await出现需要注意:

  1. 将后续代码挤入微任务队列:当await关键字出现在async函数中时,它会暂停当前async函数的执行,直到其等待的 Promise 对象被解决(resolved)或被拒绝(rejected)。并且,await后面的代码会被放入微任务队列中,等待当前任务执行完毕后执行。

  2. 提前执行时间:从效果上看,浏览器会将await的执行时间提前,使得await后面的代码在逻辑上可以当作同步代码来看待。这种特性让异步代码的编写更加简洁、直观,就像编写同步代码一样,极大地提高了代码的可读性和可维护性。

练习

console.log('script start');
async function async1() {
  await async2() // 
  console.log('async1 end'); 
}
async function async2() {
  console.log('async2 end');
}
async1()
setTimeout(() => {
  console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
.then(() => {
  console.log('then1');
})
.then(() => {
  console.log('then2');
});
console.log('script end');

2025-06-30T06_47_16.691Z-435769.gif