为什么一定要有微任务,直接一个宏任务不行吗

前言

  • 常网IT源码上线啦!
  • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。

前几天,Manus创始人肖弘,发了这样一段话。今天仍旧在AI的早期,为了让更多人用上而放弃短期的收入,甚至调整商业模式的,都是一件可以被做的事情。想要在全球化的市场里做好产品,有很多不是来自业务本身和用户价值本身的烦恼,偶尔也会想起上大学时的偶像发饭否的那句话,「多少艰苦不可告人」。但这一切是值得的。一方面因为旅程本身就有很多开心的、让自己和团队成长的事情。另外一方面,如果最后有不错的结果,证明作为中国出生的创始人,也能在新的环境下做好全球化的产品,那就太好了!

多少艰苦不可告人。

一、前言

大家在写setTimeout宏任务的时候,不知道有没有想过这个问题:为什么一定要有微任务,直接一个宏任务不行吗?

我想过,人要善于思考,才能更深入。

想象这样一个场景:你在一家繁忙的咖啡馆点单。如果所有顾客都按照"先来先服务"的原则排队,那么一个点单耗时很长的顾客(比如要定制复杂饮品)会让后面所有只想要一杯浓缩咖啡的顾客等待很久。显然,这不是最高效的方式。

聪明的我作为咖啡师会采用优先处理简单订单的策略------这就是微任务的设计理念。

直入正文。

二、为什么js一定是单线程?

JS为浏览器服务的。

浏览器对JS说,你要想服务我,就要约法三章:

  • UI交互敏感性:用户点击、滚动等操作需要即时响应

  • DOM操作安全性:避免多线程同时操作DOM导致的竞态条件

  • 轻量级特性:作为网页脚本语言需要快速启动和执行

JS听完,哦,那我只能采用单线程模型了。但面临一个根本性挑战:如何处理耗时操作而不阻塞主线程?

于是,JavaScript处理异步的方式经历了几个重要阶段:

这种演进过程中,微任务机制发挥了关键作用,使Promise和Async/Await能够优雅地实现。

二、事件循环

接下来,会聊到事件循环。

解释一下:

调用栈(Call Stack)

  • LIFO(先进后出)结构

  • 存储函数执行上下文

  • 栈溢出保护(如递归深度限制)

任务队列系统

java 复制代码
// 宏任务示例
setTimeout(() => console.log('宏任务1'), 0);
setTimeout(() => console.log('宏任务2'), 0);

// 微任务示例
Promise.resolve().then(() => console.log('微任务1'));
Promise.resolve().then(() => console.log('微任务2'));

// 输出顺序:
// 微任务1
// 微任务2
// 宏任务1
// 宏任务2

宏任务与微任务的本质区别

特性 宏任务 (MacroTask) 微任务 (MicroTask)
触发源 setTimeout, setInterval Promise.then/catch/finally, MutationObserver
队列位置 单独的任务队列 附加在每个宏任务后的微任务队列
优先级 高(优先于宏任务执行)
执行时机 事件循环的每个迭代(一轮循环执行一个宏任务) 当前宏任务结束后立即执行(清空微任务队列)
示例 I/O回调、UI渲染 Promise回调、queueMicrotask

事件循环的完整生命周期

  1. 执行一个宏任务

  2. 执行所有微任务(直到微任务队列清空)

  3. 渲染更新(如果需要)

  4. 检查Web Worker消息

  5. 进入下一个循环

java 复制代码
console.log('脚本开始'); // 宏任务1

setTimeout(() => {
  console.log('setTimeout'); // 宏任务2
  Promise.resolve().then(() => console.log('setTimeout中的微任务'));
}, 0);

Promise.resolve().then(() => {
  console.log('Promise1'); // 微任务1
  Promise.resolve().then(() => console.log('Promise1中的微任务')); // 微任务3
});

Promise.resolve().then(() => console.log('Promise2')); // 微任务2

console.log('脚本结束'); // 宏任务1继续

/* 输出顺序:
脚本开始
脚本结束
Promise1
Promise2
Promise1中的微任务
setTimeout
setTimeout中的微任务
*/

讲原理是会枯燥一点的,但耐心看下去,多看几遍,知识就是你的了,Get~

三、为什么需要微任务?单异步队列的缺陷

假设只有一种异步任务队列:

java 复制代码
// 模拟只有宏任务的世界
setTimeout(() => {
  console.log('长时间操作完成');
}, 10000); // 10秒任务

setTimeout(() => {
  console.log('紧急更新');
}, 100); // 0.1秒任务

在单队列系统中,即使紧急更新任务早已完成,也必须等待10秒才能执行------这就是优先级反转

考虑UI更新场景:

java 复制代码
// 没有微任务时
element.addEventListener('click', () => {
  // 同步更新UI状态
  updateUI(); 
  
  // 记录分析数据(非关键)
  setTimeout(logAnalytics, 0);
});

如果logAnalytics是宏任务,它可能在UI渲染后执行,但用户期望的是UI更新后立即看到结果。

微任务机制本质上解决了两个关键需求:

  1. 任务优先级:确保高优先级任务先执行

  2. 执行连续性:保证相关操作原子性

四、微任务与页面渲染

避免布局抖动

这个一般在提到回流的时候,我会经常提到的一点优化。

java 复制代码
// 错误做法:导致多次强制布局
function resizeAll(items) {
  for (let i = 0; i < items.length; i++) {
    items[i].style.width = `${container.offsetWidth}px`;
  }
}

// 正确做法:批处理布局操作
function resizeAll(items) {
  // 1. 先读取
  const width = container.offsetWidth;
  
  // 2. 再写入
  for (let i = 0; i < items.length; i++) {
    items[i].style.width = `${width}px`;
  }
}

使用requestAnimationFrame

java 复制代码
function animate() {
  // 在下一帧渲染前更新
  requestAnimationFrame(() => {
    element.style.transform = `translateX(${position}px)`;
    position += 1;
    
    if (position < 100) {
      animate();
    }
  });
}

五、框架中使用微任务

vue内部响应式改变数据,不会马上更新页面的,而是会添加到队列中。

java 复制代码
// Vue 3源码简化版
const queue = [];
let isFlushing = false;

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job);
  }
  
  if (!isFlushing) {
    isFlushing = true;
    queueMicrotask(flushJobs);
  }
}

function flushJobs() {
  try {
    for (let i = 0; i < queue.length; i++) {
      queue[i]();
    }
  } finally {
    isFlushing = false;
    queue.length = 0;
  }
}

react也是常说的,React 18引入的并发模式本质上是更精细的任务调度:

java 复制代码
// 简化版调度逻辑
function scheduleTask(task, priority) {
  if (priority === 'high') {
    queueMicrotask(task);
  } else {
    requestIdleCallback(task);
  }
}

浏览器如何防止无限递归?

  1. 每个宏任务最多执行1000个微任务(各浏览器不同)

  2. 达到限制后暂停执行,先运行下一个宏任务

java 复制代码
function recursiveMicrotask() {
  queueMicrotask(() => {
    console.log('执行微任务');
    recursiveMicrotask(); // 危险!
  });
}

微任务与Web Workers

java 复制代码
// 主线程
const worker = new Worker('worker.js');
worker.postMessage('start');

// worker.js
self.onmessage = function(e) {
  // 执行耗时计算
  const result = heavyCalculation();
  
  // 通过微任务优雅回传结果
  Promise.resolve().then(() => {
    self.postMessage(result);
  });
};

在Node.js中,process.nextTick比Promise具有更高优先级:

java 复制代码
// Node.js中的执行顺序
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

// 输出:
// nextTick
// Promise

微任务风暴问题

这也是我在面试的时候常说的,大任务分成若干个小任务执行,一个优化卡顿问题。

java 复制代码
// 可能造成页面卡顿的代码
function processLargeArray(items) {
  items.forEach(item => {
    Promise.resolve().then(() => {
      // 处理每个项目
    });
  });
}

// 优化方案:分批处理
async function batchProcess(items, batchSize = 100) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    
    // 使用await让出主线程
    await new Promise(resolve => {
      queueMicrotask(() => {
        processBatch(batch);
        resolve();
      });
    });
  }
}

以上代码只是大概意思,部分函数没写出来,意思到了就好。

内存泄漏问题

java 复制代码
// 微任务中意外保留大对象
function processData() {
  const largeData = getLargeData(); // 大对象
  
  Promise.resolve().then(() => {
    // 使用largeData
    console.log(largeData.length);
    
    // 问题:largeData在微任务完成前不会被释放
  });
}

六、说几个优化点

  1. 限制微任务数量:避免在循环中创建大量微任务

  2. 注意闭包引用:及时释放不再需要的大对象

  3. 优先使用async/await:而非直接操作微任务队列

  4. 监控任务时长:使用Performance API检测微任务执行时间

至此撒花~

后记

回到最初的问题:为什么一定要有微任务,直接一个宏任务不行吗,主要有几点:

优先级需求:微任务提供了高优先级通道,确保关键操作及时执行

原子性保证:保证相关操作的连续性,避免中间状态暴露

性能优化:减少不必要的渲染和布局计算

系统稳定性:防止长任务阻塞关键更新

使Promise、Async/Await等高级抽象成为可能

微任务机制本质上是对现实世界优先级处理的一种模拟------就像急诊室会优先处理危重病人,而不是严格按挂号顺序就诊。这种设计不是增加复杂性,而是为了解决单异步队列无法满足的实时性需求。

存在即合理。

我们在实际项目中或多或少遇到一些奇奇怪怪的问题。

自己也会对一些写法的思考,为什么不行🤔,又为什么行了?

最后,祝君能拿下满意的offer。

我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

vue3对组件通信做了哪些升级?

别在傻傻分不清any void never unknown的场景啦

Vue性能优化:从加载提速到运行时优化

vue2和Vue3和React的diff算法展开说说:从原理到优化策略

玩转Vue插槽:从基础到高级应用场景(内含为何Vue 2 不支持多根节点)

前端哪有什么设计模式

小小导出,我大前端足矣!

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/753648...

相关推荐
lichenyang45319 分钟前
从0开始的中后台管理系统-5(userList页面功能实现)
前端·javascript·vue.js
海域云SeaArea_25 分钟前
redis集群-本地环境
前端·bootstrap·html
熊猫钓鱼>_>1 小时前
腾讯云EdgeOne Pages深度使用指南
javascript·云计算·腾讯云
LLLLYYYRRRRRTT1 小时前
MariaDB 数据库管理与web服务器
前端·数据库·mariadb
胡gh1 小时前
什么是瀑布流?用大白话给你讲明白!
前端·javascript·面试
C4程序员1 小时前
北京JAVA基础面试30天打卡06
java·开发语言·面试
universe_011 小时前
day22|学习前端ts语言
前端·笔记
teeeeeeemo1 小时前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
Zz_waiting.1 小时前
Javaweb - 14.1 - 前端工程化
前端·es6
掘金安东尼1 小时前
前端周刊第426期(2025年8月4日–8月10日)
前端·javascript·面试