前言
- 常网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 |
事件循环的完整生命周期
-
执行一个宏任务
-
执行所有微任务(直到微任务队列清空)
-
渲染更新(如果需要)
-
检查Web Worker消息
-
进入下一个循环
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更新后立即看到结果。
微任务机制本质上解决了两个关键需求:
-
任务优先级:确保高优先级任务先执行
-
执行连续性:保证相关操作原子性
四、微任务与页面渲染
避免布局抖动
这个一般在提到回流的时候,我会经常提到的一点优化。
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);
}
}
浏览器如何防止无限递归?
-
每个宏任务最多执行1000个微任务(各浏览器不同)
-
达到限制后暂停执行,先运行下一个宏任务
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在微任务完成前不会被释放
});
}
六、说几个优化点
-
限制微任务数量:避免在循环中创建大量微任务
-
注意闭包引用:及时释放不再需要的大对象
-
优先使用async/await:而非直接操作微任务队列
-
监控任务时长:使用Performance API检测微任务执行时间
至此撒花~
后记
回到最初的问题:为什么一定要有微任务,直接一个宏任务不行吗,主要有几点:
优先级需求:微任务提供了高优先级通道,确保关键操作及时执行
原子性保证:保证相关操作的连续性,避免中间状态暴露
性能优化:减少不必要的渲染和布局计算
系统稳定性:防止长任务阻塞关键更新
使Promise、Async/Await等高级抽象成为可能
微任务机制本质上是对现实世界优先级处理的一种模拟------就像急诊室会优先处理危重病人,而不是严格按挂号顺序就诊。这种设计不是增加复杂性,而是为了解决单异步队列无法满足的实时性需求。
存在即合理。
我们在实际项目中或多或少遇到一些奇奇怪怪的问题。
自己也会对一些写法的思考,为什么不行🤔,又为什么行了?
最后,祝君能拿下满意的offer。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
别在傻傻分不清any void never unknown的场景啦
vue2和Vue3和React的diff算法展开说说:从原理到优化策略
玩转Vue插槽:从基础到高级应用场景(内含为何Vue 2 不支持多根节点)