JavaScript 执行机制详解:从进程线程到事件循环

作者:流萤时间:2026-10-24 20:12:10浏览:74

本文详细解析 JavaScript 的单线程执行模型,涵盖进程与线程的区别、浏览器内部多进程架构及渲染线程机制。重点阐述同步与异步任务的划分,深入剖析宏任务与微任务的执行优先级,并通过完整案例演示事件循环(Event Loop)中执行栈与任务队列的交互过程,帮助开发者彻底掌握代码执行顺序。

前言:

无论是日常开发工作还是技术面试,明确代码的执行顺序都是高频需求。因此,本文旨在系统梳理并彻底厘清 JavaScript 的执行机制。

为深入理解 JavaScript 的执行机制,需先掌握以下基础知识(以浏览器环境为例,Node.js 环境存在差异):

1.进程与线程的概念

  • 浏览器工作原理
  • 事件循环(Event-Loop)、任务队列(含同步任务、异步任务、微任务、宏任务)
  • 进程与线程

计算机的核心是 CPU,负责所有计算任务;操作系统作为管理者,负责任务调度、资源分配与管理,统筹硬件;应用程序则是具备特定功能的程序,运行于操作系统之上。

进程:

进程是操作系统进行资源分配和调度的独立单位,也是应用程序运行的载体。它是一个具有独立功能的程序在特定数据集上的一次动态执行过程,是拥有资源和独立运行的最小单位,同时也是程序执行的最小单位。

进程具有以下特征:

  • 动态性:进程是程序的一次执行过程,具有临时性和生命周期,表现为动态产生与动态消亡;
  • 并发性:多个进程可以与其他进程并发执行;
  • 独立性:进程是系统进行资源分配和调度的独立单位;
  • 结构性:进程由程序、数据和进程控制块(PCB)三部分组成。

线程:

线程是程序执行中单一的顺序控制流程,是程序执行流的最小单元,也是处理器调度和分派的基本单位。一个进程可包含一个或多个线程,各线程间共享所在进程的内存空间。标准线程由线程 ID、当前指令指针(PC)、寄存器和堆栈组成;而进程由内存空间(代码、数据、进程空间、打开的文件)及一个或多个线程构成。

进程与线程的区别:

  • 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  • 一个进程由一个或多个线程组成,线程代表进程中代码的不同执行路线;
  • 进程间相互独立,但同一进程下的各线程共享内存空间(包括代码段、数据集、堆等)及部分进程级资源(如打开的文件和信号),进程间彼此不可见;
  • 调度和切换:线程上下文切换的速度远快于进程上下文切换。

JS 为什么是单线程?

JavaScript从诞生之初即作为浏览器脚本语言,主要用于处理用户交互及 DOM 操作,这决定了它必须是单线程的,否则将引发复杂的同步问题。

举例说明:若 JS 为多线程,当一个线程修改 DOM 元素而另一个线程删除该元素时,浏览器将无法确定执行哪个操作。为避免这种复杂性,JavaScript 自诞生起便被设计为单线程。

为利用多核 CPU 算力,HTML5 提出了 Web Worker 标准,允许 JS 创建多个线程。但子线程完全受主线程控制,且禁止操作 DOM。因此,该标准并未改变 JavaScript 单线程的本质。

2.浏览器原理

作为前端工程师,对浏览器应不陌生,且现代浏览器是多进程的。

浏览器组成部分:

  • 用户界面:包括地址栏、前进/后退/刷新/书签等按钮;
  • 浏览器引擎:负责在用户界面与呈现引擎之间传送指令;
  • 渲染引擎:负责绘制请求的内容;
  • 网络:负责完成网络调用(如 HTTP 请求),具有平台无关接口,可在不同平台工作;
  • JavaScript 解释器:负责解析执行JavaScript代码;
  • 用户界面后端:用于绘制基本窗口部件(如下拉框、窗口),底层使用操作系统用户接口;
  • 数据存储:属于持久层,浏览器在硬盘中保存类似cookie等数据。HTML5 定义了 Web Database 技术,这是一种轻量级完整的客户端存储技术;

⚠️注意:与大多数浏览器不同,谷歌(Chrome)浏览器的每个标签页分别对应一个呈现引擎实例。每个标签页都是一个独立进程。

浏览器包含哪些进程?

浏览器进程:

  • 浏览器主进程(负责协调与主控),该进程只有一个;
  • 负责浏览器界面显示及用户交互(如前进、后退等);
  • 负责各页面的管理,包括创建和销毁其他进程;
  • 将渲染(Renderer)进程得到的内存中的Bitmap(位图)绘制到用户界面上;
  • 负责网络资源管理及下载等;

第三方插件进程:

  • 负责管理第三方插件;

GPU 进程:

  • 负责 3D 绘制与硬件加速(最多一个);

渲染进程:

  • 负责页面文档解析、执行与渲染;

渲染进程包含哪些线程?

GUI 渲染线程:

  • 主要负责解析 HTML、CSS,构建 DOM 树,布局及绘制等;
  • 该线程与 JavaScript 引擎线程互斥。当执行JavaScript引擎线程时,GUI 渲染线程会被挂起;仅当任务队列空闲时,主线程才会执行 GUI 渲染;

JavaScript 引擎线程:

  • 主要负责处理JavaScript脚本,执行代码(如 V8 引擎);
  • 浏览器同时只能有一个 JS 引擎线程在运行 JS 程序,即 JS 是单线程的;
  • JS 引擎线程与 GUI 渲染线程互斥,因此 JS 引擎会阻塞页面渲染;

定时触发器线程:

  • 负责执行定时器函数(setTimeout,setInterval);
  • 浏览器定时计数器并非由 JS 引擎计数(因 JS 单线程,若处于阻塞状态会影响计数器准确性);
  • 通过单独线程计时并触发定时(计时完毕后,将事件添加到事件触发线程的事件队列中,等待 JS 引擎空闲后执行)。该线程即定时触发器线程,也称定时器线程;
  • W3C 在 HTML 标准中规定,要求setTimeout中低于 4ms 的时间间隔算为 4ms;

事件触发线程:

  • 负责将准备好的事件交给 JS 引擎线程执行;
  • 当事件被触发时,该线程会将对应事件添加到待处理队列的队尾,等待 JS 引擎处理;

异步请求线程:

  • 在XMLHttpRequest连接后浏览器会开启一个线程;
  • 检测请求状态变更时,若有对应回调函数,异步请求线程会产生状态变更事件,并将对应回调函数放入队列等待 JS 引擎执行;

3.同步与异步

由于JavaScript是单线程的,这决定了其任务不可能只有同步任务。若耗时较长的任务也按同步执行,将导致页面阻塞;

因此 JavaScript 任务一般分为两类:

同步任务:

同步任务指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务:

异步任务指不进入主线程、而进入“任务队列”(Event queue)的任务。只有“任务队列”通知主线程某个异步任务可执行时,该任务才会进入主线程执行。

常见的异步任务:定时器、ajax、事件绑定、回调函数、promise、async await等;

  • 同步和异步任务分别进入不同的执行“场所”:同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当 Event Table 中指定的事情完成时,会将该函数移入 Event Queue。
  • 主线程内的任务执行完毕为空时,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,即常说的 Event Loop(事件循环)。
  • 我们不禁要问:如何知道主线程执行栈为空?JS 引擎存在monitoring process进程,会持续不断检查主线程执行栈是否为空。一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

宏任务与微任务:

JavaScript 除了广义上的同步任务与异步任务,还有更精细的任务定义:

  • 宏任务(macro-task):包括全局代码、setTimeout、setInterval;
  • 微任务(micro-task):new Promise().then(回调)、process.nextTick();

不同类型的任务会进入到不同的任务队列:

1.进程与线程的概念

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

4.执行栈与任务队列

执行栈:

JavaScript 代码都是在执行上下文中执行的,在 JavaScript 中有三种执行上下文:

  • 全局执行上下文
  • 函数执行上下文,JS 函数被调用时都会创建一个函数执行上下文
  • eval 执行上下文,eval 函数产生的上下文(用的较少)

通常来说我们的 JS 代码都不止一个上下文,那么这些上下文的执行顺序是怎样的呢?

我们知道栈是一种后进先出的数据结构,我们JavaScript中的执行栈就是一种这样的栈结构。当 JS 引擎执行代码时,会产生一个全局上下文并把它压入执行栈;每当遇到函数调用时,就会产生函数执行上下文并压入执行栈。引擎从栈顶开始执行函数,执行完后会弹出该执行上下文。


function add(){
  console.log(1)
  foo()
  console.log(3)
}

function foo(){
  console.log(2)
}
add()

我们来看下上面这段代码的执行栈是怎样的:

1.进程与线程的概念

任务队列:

前面我们说到了JavaScript中所有的任务分为同步任务与异步任务。同步任务,顾名思义就是立即执行的任务,它一般是直接进入到主线程中执行。而我们的异步任务则是进入任务队列等待主线程中的任务执行完再执行。

任务队列是一个事件的队列,表示相关的异步任务可以进入执行栈了。主线程读取任务队列就是读取里面有哪些事件。

队列是一种先进先出的数据结构。

上面我们说到异步任务又可以分为宏任务与微任务,所以任务队列也可以分为宏任务队列与微任务队列;

  • Macrotask Queue:进行比较大型的工作,常见的有setTimeout、setInterval、用户交互操作、UI 渲染等;
  • Microtask Queue:进行较小的工作,常见的有Promise、Process.nextTick;

5.事件循环(Event-Loop)

  • 同步任务直接放入到主线程执行,异步任务(点击事件、定时器、ajax 等)挂在后台执行,等待 I/O 事件完成或行为事件被触发。
  • 系统后台执行异步任务,如果某个异步任务事件(或者行为事件被触发),则将该任务添加到任务队列,并且每个任务会对应一个回调函数进行处理。
  • 这里异步任务分为宏任务与微任务,宏任务进入到宏任务队列,微任务进入到微任务队列。
  • 执行任务队列中的任务具体是在执行栈中完成的。当主线程中的任务全部执行完毕后,去读取微任务队列,如果有微任务就会全部执行,然后再去读取宏任务队列;
  • 上述过程会不断的重复进行,也就是我们常说的事件循环(Event-Loop)。

1.进程与线程的概念

例题验证:

我们来看道题目进行验证


(async ()=>{
    console.log(1) 
  
    setTimeout(() => {
    console.log('setTimeout1')
    }, 0);
  
    function foo (){
        return new Promise((res,rej) => {
            console.log(2)
            res(3)
        })
    }
  
    new Promise((resolve,reject)=>{
    console.log(4)
    resolve() 
    console.log(5)
    }).then(()=> {
    console.log('6')
    })
  
    const res = await foo();
    console.log(res);
    console.log('7')
  
    setTimeout(_ => console.log('setTimeout2'))
})()

打印顺序是:1,4,5,2,6,3,7,setTimeout1,setTimeout2

分析:

  • 代码自上而下执行,先遇到console.log(1),直接打印 1,接着遇到定时器属于宏任务,放入宏任务队列;
  • 再遇到 promise,由于new Promise是一个同步任务,所以直接打印 4。遇到 resolve,也就是后面的then函数,放入微任务队列,再打印 5;
  • 然后再执行 await foo,foo 函数里面有个promise,new promise属于同步任务,所以会直接打印 2。await 返回的是一个promise的回调,await 后面的任务放入微任务队列;
  • 最后遇到一个定时器,放入宏任务队列;
  • 执行栈任务执行完了,先去微任务队列获取微任务执行。先执行第一个微任务,打印 6;再执行第二个微任务,打印 3、7;
  • 微任务执行完,再去宏任务队列获取宏任务执行,打印setTimeout1、setTimeout2;

6.定时器

JavaScript的任务队列中的异步任务还包括定时器事件,即指定某些代码在多长时间后执行。定时器功能主要由setTimeout()和 setInterval()两个函数来完成,它们的内部执行机制完全一样,区别主要在于setTimeout是一次执行的过程,setInterval则是反复执行的过程。

setTimeout函数接受两个参数,第一个是要执行的回调函数,第二个是推迟执行的时间(ms);

如果我们把推迟时间设为 0,是不是就会立即执行呢?


setTimeout(()=>{
    console.log(1)
},0)

console.log(2)

但事实并不是这样的,上面的打印结果是先打印 2,再打印 1。是不是觉得很蒙?

我们用上面的事件循环的规则来理解就很清晰了:全局代码执行,遇到定时器setTimeout,放入宏任务队列,接着往下执行同步代码,打印 2。执行栈任务执行完再去微任务队列,没有微任务再去看宏任务队列,有一个宏任务,执行打印 1。

setTimeout(fn,0) 的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的事件都处理完,才会得到执行。

HTML5 标准规定了 setTimeout() 的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。另外,对于那些 DOM 的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每 16 毫秒执行一次。这时使用requestAnimationFrame()的效果要好于 setTimeout()。

需要注意的是,setTimeout()只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

到此这篇关于 JavaScript 执行机制详细介绍的文章就介绍到这了,更多相关 JavaScript 执行机制内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章