深入理解JavaScript事件循环机制与线程模型

作者:竹影时间:2026-10-27 01:16:14浏览:520

本文解析JavaScript为何采用单线程模型,梳理浏览器多进程架构及渲染进程内的五大核心线程。重点阐述事件循环(Event Loop)机制,区分同步与异步任务,详解宏任务与微任务的执行顺序,并通过代码示例剖析setTimeout、Promise等异步操作的最终输出结果。

JavaScript之所以被设计为单线程语言,核心原因在于其作为浏览器脚本的主要职责是与用户交互及操作DOM。若允许JS拥有多个线程,当两个线程同时修改同一DOM元素(例如一个添加内容,另一个移除)时,浏览器将无法确定最终状态,从而引发不可预知的错误。因此,单线程模型是保证DOM操作一致性的必要约束。

JavaScript作为浏览器脚本语言,主要用途就是和用户互动和操作DOM。比如js同时有两个线程,两个线程同时操作同一个DOM,比如一个给DOM添加内容,一个移除DOM,那到底该听谁的呢?所以这就决定了它只能是单线程,否则就会出现一些奇怪的问题。

浏览器进程架构概述

每当用户打开一个新的标签页,浏览器便会创建一个独立的进程来承载该页面。

浏览器内部包含多种进程类型,各自承担不同职责。

浏览器进程是浏览器的核心控制单元,负责协调各子进程的工作。

  • 浏览器主进程仅有一个,它是整个浏览器的协调者和主控中心。
  • 负责浏览器界面的显示与用户交互,包括前进、后退等导航操作。
  • 负责管理各个标签页的生命周期,包括创建、销毁及其他子进程的管理。
  • 负责将渲染进程生成的内存位图(Bitmap)绘制到用户界面上。
  • 负责网络资源的管理与下载任务。

第三方插件进程用于隔离插件,防止其崩溃影响主浏览器。

每种类型的插件对应一个独立进程,仅在插件被使用时创建。由于插件容易崩溃,通过进程隔离可确保插件崩溃不会波及浏览器主进程或页面内容。

GPU进程专注于图形处理,如3D绘制等高性能计算任务。

GPU进程同样只有一个,专门用于处理3D绘制等图形相关任务。

渲染进程(Renderer Process)通常被称为浏览器内核,内部包含多个线程。

  • 渲染进程(Renderer Process)通常被称为浏览器内核,内部包含多个线程。
  • 每个标签页拥有独立的渲染进程,彼此之间互不干扰。
  • 渲染进程主要负责页面渲染、脚本执行及事件处理等核心功能。

网络进程独立负责页面所需网络资源的加载工作。

网络进程主要负责页面的网络资源加载。

若浏览器采用单进程架构,任一标签页或插件崩溃均会导致整个浏览器失效。现代浏览器包含多个进程,每个进程内含众多线程,共同消耗内存资源。进程间实行严格的数据隔离机制,旨在保障操作系统中各任务互不干扰。每个进程仅能访问自身数据,从而杜绝进程A向进程B写入数据的可能性。正因进程间数据严格隔离,单个进程的崩溃或挂起不会波及其他进程。

渲染进程详解

页面渲染、JavaScript执行及事件循环均在渲染进程中完成,因此需重点关注渲染进程的内部机制。渲染进程是多线程的,主要包含以下几个关键线程。

GUI线程负责浏览器界面的渲染工作。

负责解析HTML与CSS,构建DOM树和RenderObject树,执行布局计算及绘制操作。

当修改元素的颜色或背景色等样式属性时,会触发页面的重绘(Repaint)。

当修改元素的尺寸、字体等影响布局的属性时,会触发重排,也称为回流(Reflow)。

仅在页面需要重绘或重排时,GUI线程才会执行绘制操作。

重绘和重排的计算成本较高,开发中应尽量避免不必要的重绘和重排以提升性能。

GUI线程与JS引擎线程是互斥关系,二者不能同时运行。

  • 当JS引擎正在执行脚本时,GUI线程会被挂起,停止渲染工作。
  • GUI的更新操作会被保存到一个队列中,待JS引擎执行完毕后立即执行。

JS引擎线程是JavaScript的核心执行环境。

JS引擎线程即JS内核(如V8引擎),负责处理JavaScript脚本程序。

JS引擎线程负责解析JavaScript脚本并运行代码。

JS引擎持续等待任务队列中的任务,一旦有任务到达便立即处理。

  • 浏览器在同一时刻仅允许一个JS引擎线程运行JS程序,这决定了JavaScript的单线程特性。
  • 在任何标签页的渲染进程中,无论何时都只有一个JS线程在运行JS程序。

GUI线程与JS引擎线程互斥,JS引擎的执行会阻塞GUI渲染线程。

若JS执行时间过长,将导致页面渲染不连贯,进而造成页面加载阻塞。

事件触发线程属于浏览器而非JS引擎,负责控制事件循环及事件队列。

  • 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue)。
  • 当JS引擎执行事件绑定或异步操作(如setTimeout),或浏览器其他线程(如鼠标点击、Ajax请求)产生事件时,由事件触发线程将对应事件添加到相应线程(如定时器线程)。异步事件完成后,其回调操作被添加到事件队列,等待JS引擎空闲时处理。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
  • 由于JS是单线程的,事件队列中的事件必须排队等待JS引擎依次处理。

定时触发器线程专门负责管理定时器相关任务。

  • setInterval与setTimeout所在线程。
  • 浏览器的定时计数器并非由JS引擎计数,因为JS引擎单线程且可能阻塞,这会影响计时的准确性。
  • 通过单独线程进行计时并触发定时(计时完毕后,将事件添加到事件触发线程的事件队列中,等待JS引擎空闲后执行)。
  • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

异步HTTP请求线程负责处理XMLHttpRequest连接后的网络请求。

  • 在XMLHttpRequest在连接后是通过浏览器新开的一个线程请求。
  • 当检测到状态变更时,若设置了回调函数,异步线程将产生状态变更事件,将该回调放入事件队列,再由JS引擎执行。
  • 简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行。

接下来探讨JavaScript的核心机制:事件循环。

事件循环(Event Loop)是JavaScript异步模型的核心。

  • JavaScript任务被分为同步任务和异步任务两类。
  • 同步任务在主线程(JS引擎线程)上执行,形成一个执行栈。
  • 除主线程外,事件触发线程管理任务队列,异步任务完成后,其回调会被放入任务队列。
  • 当执行栈中所有同步任务执行完毕后,JS引擎会读取任务队列,将可运行的异步任务(即任务队列中的事件回调)添加到执行栈中开始执行。

JavaScript之所以被设计为单线程语言,核心原因在于其作为浏览器脚本的主要职责是与用户交互及操作DOM。若允许JS拥有多个线程,当两个线程同时修改同一DOM元素(例如一个添加内容,另一个移除)时,浏览器将无法确定最终状态,从而引发不可

let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}

// 同步任务
console.log('我是同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get('/info',httpCallback);

// 同步任务
console.log('我是同步任务2');

通过代码示例解析具体的执行过程。

  • js会从上到下依次执行,可以先理解为这段代码的执行环境就是主线程,也就是当前执行栈。
  • 首先 执行console.log('我是同步任务1');
  • 然后执行到setTimeout时候,会交给定时器线程,并告诉定时器线程在1s后将setTimeoutCallBack回调交给事件触发线程,1s后事件触发线程把这个回调添加到了任务队列中等待执行。
  • 接着执行ajax,会交给异步HTTP请求线程发送网络请求,请求成功后,将回调httpCallback交给事件触发线程并放入任务队列中等待执行。
  • 接着执行console.log('我是同步任务2');
  • 此时主线程执行栈执行完毕,js引擎线程已经空闲,开始询问事件触发线程的任务队列中是否有需要执行的回调,如果有则将任务队列中的回调事件加入执行栈中,开始执行,如果任务队列中没有需要执行的回调,js引擎会不断的发起询问,直到有为止。

浏览器上的所有线程行为单一,职责明确。

  • 定时触发线程只管理定时器并只关心定时不关注结果,定时结束后就把回调交给事件触发线程。
  • 异步HTTP请求线程只关心http请求不关心结果,请求结束后就把回调交给时间触发线程。
  • 事件触发线程只将异步回调加入事件队列。
  • JS引擎线程执行执行栈中的事件,执行栈代码执行完毕后,询问事件触发线程的事件队列是否有回调需要执行,然后将事件队列中的事件添加到执行栈中执行,这种反复循环的行为称为事件循环。

了解事件循环后,进一步区分宏任务与微任务。

宏任务与微任务的分类与区别。

宏任务(Macrotask)是较大的异步任务单元。

  • 渲染事件(如解析 DOM、计算布局、绘制)。
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)。
  • JavaScript 脚本执行事件。
  • 网络请求完成、文件读写完成事件。

为了协调这些任务能够有序的在主线程上执行,页面进行引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,主线程不断的从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。

常见的宏任务包括以下类型。

  • 主代码块(整体脚本执行)。
  • setTimeOut。
  • setInterval。
  • setImmediate -- node环境特有。
  • requestAnimationFrame -- 浏览器 JS引擎线程和GUI渲染线程是互斥的,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务结束后,在一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

JavaScript之所以被设计为单线程语言,核心原因在于其作为浏览器脚本的主要职责是与用户交互及操作DOM。若允许JS拥有多个线程,当两个线程同时修改同一DOM元素(例如一个添加内容,另一个移除)时,浏览器将无法确定最终状态,从而引发不可

微任务(Microtask)是优先级更高的异步任务。

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束后之前。

异步回调主要有两种处理方式。

  • 把异步回调函数封装成一个宏任务,添加到消息队列中,当循环系统执行到该任务的时候执行回调函数。
  • 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务的形式体现的。

我们知道宏任务结束后,会执行渲染,然后执行下一次宏任务,微任务可以理解为当前宏任务执行后立即执行的任务。

常见的微任务包括以下类型。

  • process.nextTick()--node环境特有。
  • Promise.then()。
  • catch。
  • finally。
  • Object.observe(已废弃,但历史上属于微任务)。
  • MutationObserver。

当执行完一个宏任务之后,会立即执行期间所产生的所有微任务,然后执行渲染。

JavaScript之所以被设计为单线程语言,核心原因在于其作为浏览器脚本的主要职责是与用户交互及操作DOM。若允许JS拥有多个线程,当两个线程同时修改同一DOM元素(例如一个添加内容,另一个移除)时,浏览器将无法确定最终状态,从而引发不可

宏任务与微任务的执行流程遵循特定顺序。

浏览器首先会执行一个宏任务,然后执行当前执行栈所产生的微任务,然后再渲染页面,然后再执行下一个宏任务。

JavaScript之所以被设计为单线程语言,核心原因在于其作为浏览器脚本的主要职责是与用户交互及操作DOM。若允许JS拥有多个线程,当两个线程同时修改同一DOM元素(例如一个添加内容,另一个移除)时,浏览器将无法确定最终状态,从而引发不可

通过经典面试题深入理解执行流程。

function test() {
  console.log(1)
  setTimeout(function () { 	// timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () { 		// timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () { 	// timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () { 	// timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

下面我们来分析一下整体的流程。

首先应该找到同步任务先执行。

  • 当test()调用的时候 console.log(1)会先执行,打印1。而setTimeout(我们记作timer1)作为宏任务加入宏任务队列。
  • test下面的setTimtout(我们记作timer2)作为宏任务加入宏任务队列。
  • new Promise()的executer中中也会当做同步任务执行 所以console.log(4)打印4。而setTimeout(我们记作timer3)作为宏任务加入宏任务队列。
  • 接着promise.then()作为微任务加入微任务队列。
  • 最后console.log(8)作为同步任务执行,打印8。

我们再看异步任务的处理流程。

  • 我们当前的执行栈本身就是宏任务,宏任务执行完了之后应该立即执行微任务,这里的微任务只有Promise.then(),而setTimeout(我们记作timer4)作为宏任务加入宏任务队列,然后执行console.log(7)打印7。
  • 微任务执行完毕之后,要执行GUI渲染,我们代码中没有。
  • 执行宏任务队列,此时宏任务队列里面有 timer1、timer2、timer3、timer4。
  • 按照定时时间,可以排列为:timer2、timer4、timer3、timer1依次拿出放入执行栈末尾执行。
  • 执行timer2,console.log(3)作为同步任务打印3,然后检查有没有微任务和GUI渲染。
  • 执行timer4,console.log(6)作为同步任务打印6,然后检查有没有微任务和GUI渲染。
  • 执行timer3,console.log(5)作为同步任务打印5,然后检查有没有微任务和GUI渲染。
  • 执行timer1,console.log(2)作为同步任务打印2,然后检查有没有微任务和GUI渲染 所以最终结果为:1、4、8、7、3、6、5、2。

到此这篇关于简单聊聊JavaScript中的事件循环的文章就介绍到这了,更多相关JavaScript事件循环内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章