请不要再只会回答宏任务和微任务了

关于js的事件循环,我相信凡是从事前端工作的开发者,都有一定程度的了解,但大多都是"背书",从"js是个单线程语言"开始,到"宏任务和微任务队列,微任务优先级更高"结束。 概念其实没什么大问题,但是随着浏览器逐渐演变成仅次于操作系统的复杂应用,我们的传统观念也需要一定的更新,今天,带大家从浏览器的视角出发,看看当下的事件循环是什么样子。

浏览器的进程模型

何为进程

程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程。 每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

何为线程

有了进程后,就可以运行程序的代码了。 运行代码的「人」称之为「线程」。 一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程 。 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。

浏览器有哪些进程线程

首先要明确一点:浏览器是一个多进程多线程的应用程序 。 浏览器内部工作极其复杂。为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

可以在浏览器的任务管理器中查看当前的所有进程 其中,最主要的进程有:

  1. 浏览器进程

    主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  2. 网络进程

    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。

  3. 渲染进程(本文重点讲解的进程)

    渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。

    默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

    将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档

渲染主线程是如何工作的

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ......

关于渲染进程为什么不使用多线程来处理这么多任务,我其实推荐大家可以去看看《你不知道的javascript》中卷第二章,里面详细讲述了js为什么被设计为单线程

要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务? 比如:

  • 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
  • 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
  • 浏览器进程通知我"用户点击了按钮",与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
  • ......

渲染主线程想出了一个绝妙的主意来处理这个问题:排队。

  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务,这样一来,就可以让每个任务有条不紊的、持续的进行下去了。

整个过程,被称之为事件循环(消息循环)

何为异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 ------ setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 -- XHRFetch
  • 用户操作后需要执行的任务 -- addEventListener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」

渲染主线程承担着极其重要的工作,无论如何都不能阻塞! 因此,浏览器选择异步来解决这个问题

使用异步的方式,渲染主线程永不阻塞

任务有优先级吗

我们都知道事件循环的过程中包含宏任务和微任务的说法,经常讲,事件循环往往从宏任务开始,但是在执行下一个宏任务前,我们需要先将本轮宏任务产生的微任务执行完毕。 那么任务有优先级吗?答案是没有但是任务队列有。 根据 W3C 的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
  • 浏览器必须 准备好一个微队列 ,微队列中的任务优先所有 其他任务执行 html.spec.whatwg.org/multipage/w...

宏任务队列已经无法满足当前的浏览器要求了

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」
  • .....

以下是模拟浏览器任务调度的伪代码(由豆包生成),重点体现了三种队列的优先级关系和处理流程:

javascript 复制代码
// 模拟浏览器的三种任务队列
const queues = {
  microtasks: [],         // 微任务队列(最高优先级)
  inputQueue: [],         // 交互队列(用户输入等)
  timerQueue: [],         // 延时队列(setTimeout/setInterval)
  renderingQueue: []      // 渲染队列(额外补充,用于完整模拟)
};

// 任务调度器状态
let isProcessing = false;

// 模拟浏览器的任务处理主循环
function browserMainLoop() {
  // 持续运行的事件循环
  while (true) {
    // 1. 先处理所有微任务(最高优先级)
    processAllMicrotasks();
    
    // 2. 检查是否需要渲染(通常在微任务后考虑)
    if (shouldRender()) {
      processRenderingTasks();
    }
    
    // 3. 处理高优先级任务:交互队列(用户输入优先于定时器)
    if (queues.inputQueue.length > 0) {
      processNextTask(queues.inputQueue);
      continue;
    }
    
    // 4. 处理延时队列任务(优先级低于交互)
    if (queues.timerQueue.length > 0) {
      // 只处理已到期的定时器任务
      const now = getCurrentTime();
      const readyTimers = queues.timerQueue.filter(task => task.expires <= now);
      if (readyTimers.length > 0) {
        // 按到期时间排序,先处理最早到期的
        readyTimers.sort((a, b) => a.expires - b.expires);
        processNextTask(readyTimers);
        continue;
      }
    }
    
    // 5. 若没有任务,进入休眠等待新任务
    waitForNewTasks();
  }
}

// 处理所有微任务(执行到队列为空)
function processAllMicrotasks() {
  while (queues.microtasks.length > 0) {
    const microtask = queues.microtasks.shift();
    executeTask(microtask);
  }
}

// 处理单个任务队列中的下一个任务
function processNextTask(queue) {
  if (queue.length === 0) return;
  
  isProcessing = true;
  const task = queue.shift();
  executeTask(task);
  isProcessing = false;
  
  // 执行完一个任务后,再次检查微任务(微任务会在当前任务后立即执行)
  processAllMicrotasks();
}

// 执行任务的具体逻辑
function executeTask(task) {
  try {
    task.callback();  // 执行任务的回调函数
  } catch (error) {
    reportError(error);  // 处理任务执行中的错误
  }
}

// 辅助函数:检查是否需要渲染
function shouldRender() {
  // 简化逻辑:根据浏览器刷新频率(如60Hz约16ms一次)判断是否需要渲染
  return getCurrentTime() - lastRenderTime > 16;
}

// 模拟添加任务的API(对应浏览器提供的API)
const browser = {
  // 添加微任务(如Promise.then)
  queueMicrotask(callback) {
    queues.microtasks.push({ callback });
  },
  
  // 添加延时任务(如setTimeout)
  setTimeout(callback, delay) {
    const expires = getCurrentTime() + delay;
    queues.timerQueue.push({ callback, expires });
  },
  
  // 添加交互任务(如click事件)
  addInputTask(callback) {
    queues.inputQueue.push({ callback });
  },
  
  // 添加渲染任务
  requestAnimationFrame(callback) {
    queues.renderingQueue.push({ callback });
  }
};

核心优先级规则说明:

  1. 微任务队列(microtasks)优先级最高 - 无论其他队列是否有任务,当前执行栈空闲时会先清空所有微任务 - 对应API:Promise.thenqueueMicrotaskasync/await
  2. 交互队列(inputQueue)次之 - 用户输入(点击、键盘等)任务优先级高于定时器,保证用户操作的响应速度 - 浏览器会优先处理用户交互,避免界面卡顿感
  3. 延时队列(timerQueue)优先级较低 - setTimeout/setInterval任务仅在没有交互任务时才会被处理 - 定时器的实际执行时间可能比设定时间晚(受队列阻塞影响)
  4. 渲染任务(renderingQueue)适时执行 - 通常在微任务处理后、其他任务执行前检查是否需要渲染 - 遵循显示器刷新率(如60fps),避免过度渲染消耗性能

这个伪代码简化了浏览器的实际实现,但核心逻辑符合现代浏览器(包括Chrome)的任务调度原则:优先保证用户交互响应速度,其次处理定时任务,而微任务则始终在当前任务周期内立即完成

实际浏览器中,任务调度会更复杂,还会涉及任务优先级动态调整、线程池管理、节能策略等,但上述伪代码已能体现三种队列的核心优先级关系。

总结

本文主要从浏览器的渲染进程的视角出发,为大家讲解当前浏览器环境下 的事件循环是什么样的,如果正在阅读本文的你之前并不了解什么是事件循环,我这里推荐你阅读这篇文章,我相信读完以后,你一定能对事件循环有一定程度的了解。

相关推荐
持久的棒棒君32 分钟前
启动electron桌面项目控制台输出中文时乱码解决
前端·javascript·electron
小离a_a1 小时前
使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
前端·css
郭优秀的笔记2 小时前
抽奖程序web程序
前端·css·css3
布兰妮甜2 小时前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
小小愿望2 小时前
ECharts 实战技巧:揭秘 X 轴末项标签 “莫名加粗” 之谜及破解之道
前端·echarts
小小愿望3 小时前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css
摸着石头过河的石头3 小时前
taro3.x-4.x路由拦截如何破?
前端·taro
lpfasd1233 小时前
开发Chrome/Edge插件基本流程
前端·chrome·edge
练习前端两年半3 小时前
🚀 Vue3 源码深度解析:Diff算法的五步优化策略与最长递增子序列的巧妙应用
前端·vue.js