setTimeout设为0就马上执行?JS异步背后的秘密

张开发
2026/4/21 9:39:01 15 分钟阅读

分享文章

setTimeout设为0就马上执行?JS异步背后的秘密
你有没有遇到过这种情况代码里写了setTimeout(fn, 0)心想这下该马上执行了吧结果发现还是慢了一拍。还有为什么Promise比setTimeout先执行async/await到底在等什么今天用餐厅点餐的故事来讲讲 JavaScript 事件循环。为什么需要事件循环单线程的困境JavaScript 是单线程的——同一时间只能做一件事。就像只有一个厨师的小餐厅如果厨师做完一道菜才接下一单客人等得头发都白了。所以 JavaScript 采用了异步回调的方式点完单先去干别的菜好了再叫你。事件循环就是传唤员事件循环就像餐厅里的传唤员厨房做好了菜传唤员看看单子喊33号你的菜好了如果你正在吃饭执行其他代码传唤员就等着轮到你的时候你放下筷子执行完当前代码去取菜执行回调调用栈 — 厨师的工作台代码是怎么跑起来的当你调用一个函数这个函数就被放进调用栈里执行。就像厨师在工作台上一边做菜一边接新单做完一单马上处理下一单function cooking() { console.log(开始炒菜); fry(); console.log(炒好了); } function fry() { console.log(放油); console.log(放菜); console.log(翻炒); } cooking();执行顺序调用栈: 1. cooking() 入栈 2. console.log(开始炒菜) 入栈执行出栈 3. fry() 入栈 4. fry() 内的 console.log 依次执行 5. fry() 出栈 6. console.log(炒好了) 入栈执行出栈 7. cooking() 出栈调用栈的特点后进先出就像叠盘子最后放上去的先被用同步执行每个函数必须执行完下一个才能进来栈溢出如果递归没终止栈会无限增长直到崩溃// 栈溢出示例 function recursive() { recursive(); } recursive(); // RangeError: Maximum call stack size exceeded任务队列 — 取餐口异步代码放哪儿当遇到setTimeout、Promise、事件回调这些异步任务时它们不会马上执行而是被放到任务队列里。就像点完单服务员把单子放到取餐口等叫号再去取。事件循环的运行机制┌─────────────────────┐ │ 调用栈 │ ← 正在执行 │ (Call Stack) │ └─────────────────────┘ ↓ ┌─────────────────────┐ │ 任务队列 │ ← 等待执行 │ (Task Queue) │ └─────────────────────┘ ↓ 事件循环 (Event Loop) 栈空了好取下一个事件循环的规则首先执行调用栈里的所有同步代码调用栈清空后去任务队列取一个任务执行完成后回到步骤1console.log(1); setTimeout(() { console.log(2); }, 0); console.log(3); // 输出1 → 3 → 2 // 因为 setTimeout 的回调在任务队列要等调用栈空才能执行微任务 vs宏任务 — VIP和普通号两种不同的队任务队列其实分两种类型例子优先级宏任务MacrotasksetTimeout、setInterval、I/O、UI渲染低微任务MicrotaskPromise.then()回调、MutationObserver、queueMicrotask高就像餐厅里宏任务 普通取餐号要排队微任务 VIP会员卡来了直接优先处理注意不是 Promise 本身是微任务而是Promise.then() 的回调函数是微任务。执行顺序console.log(1); setTimeout(() { console.log(2); // 宏任务 }, 0); Promise.resolve().then(() { console.log(3); // 微任务 }); console.log(4); // 输出1 → 4 → 3 → 2 // 同步代码 → 微任务 → 宏任务完整执行流程setTimeout(() console.log(setTimeout), 0); Promise.resolve() .then(() console.log(Promise1)) .then(() console.log(Promise2)); Promise.resolve() .then(() console.log(Promise3)); console.log(同步代码); // 输出顺序 // 同步代码 // Promise1 // Promise3 // Promise2 ← Promise.then 链式调用在同一个微任务队列 // setTimeout ← 所有微任务完成后才执行宏任务嵌套的 PromisePromise.resolve().then(() { console.log(第一个微任务); Promise.resolve().then(() { console.log(嵌套的微任务); }); }); console.log(同步代码); // 输出 // 同步代码 // 第一个微任务 // 嵌套的微任务 // 微任务队列清空后才会执行下一个宏任务async/await — 语法糖的秘密async/await 是什么async/await是 Promise 的语法糖让异步代码看起来像同步代码。// Promise 写法 function getData() { return fetch(/api/user) .then(res res.json()) .then(data console.log(data)); } // async/await 写法 async function getData() { const res await fetch(/api/user); const data await res.json(); console.log(data); }await 到底在等什么await会暂停当前 async 函数的执行等待 Promise 完成然后继续执行后面的代码。暂停期间其他代码可以继续执行async function example() { console.log(1); await fetch(/api/data); // 这里暂停 console.log(3); // ← 这行去哪了 } console.log(2); example(); console.log(4); // 输出2 → 1 → 4 → 3await后面那行代码去哪了await后面的代码不会马上执行而是被包成一个微任务。等 await 的 Promise resolve 后这个微任务才会执行async function example() { console.log(1); await fetch(/api/data); // Promise pending... // 下面的代码被包成微任务要等 Promise 完成才执行 console.log(3); // ← 这行实际上是 await 的 resolve 后的回调 } // 等价于 function example() { console.log(1); return fetch(/api/data).then(() { console.log(3); // ← 这里 }); }async 函数返回值async函数总是返回一个Promiseasync function getNumber() { return 42; } getNumber().then(console.log); // 42 // 等价于 async function getNumber() { return Promise.resolve(42); }错误处理// try-catch async function fetchData() { try { const res await fetch(/api/data); const data await res.json(); } catch (error) { console.log(出错了:, error); } } // Promise catch async function fetchData() { const res await fetch(/api/data).catch(err console.log(err)); }requestAnimationFrame — 动画的正确姿势为什么不用 setIntervalsetInterval不保证什么时候执行也不保证每次间隔精确setInterval(() { moveBall(); // 可能丢帧、卡顿 }, 16); // 约60fps但不一定准requestAnimationFrame 的特点浏览器优化在下一次重绘之前执行不丢帧页面不可见时自动暂停节省性能约60fps和屏幕刷新率同步function animate() { moveBall(); requestAnimationFrame(animate); } requestAnimationFrame(animate); // 取消动画 const id requestAnimationFrame(animate); cancelAnimationFrame(id);执行顺序用户点击 ↓ 事件触发 ↓ 微任务全部清空← 先清空所有微任务 ↓ 宏任务 ↓ requestAnimationFrame ← 所有微任务清空后渲染之前 ↓ 浏览器渲染深入了解事件循环 Node.js 的事件循环Node.js 和浏览器的事件循环不一样┌───────────────────────────────────────────────────────┐ │ Node.js 事件循环 │ ├───────────────────────────────────────────────────────┤ │ ① Timers → setTimeout, setInterval 回调 │ │ ② Pending I/O → I/O callbacks延迟到下一循环 │ │ ③ Idle/Prepare → 内部使用 │ │ ④ Poll → 获取新 I/O 事件 │ │ ⑤ Check → setImmediate 回调 │ │ ⑥ Close → close 事件回调 │ └────────────────────────────────────────── ────────────┘浏览器和 Node.js 的区别// 浏览器 setTimeout(() console.log(timeout), 0); Promise.resolve().then(() console.log(microtask)); // 输出microtask → timeout // Node.js可能不同 setTimeout(() console.log(timeout), 0); Promise.resolve().then(() console.log(microtask)); // 可能输出microtask → timeout // 但 setImmediate 可能更早queueMicrotask vs Promise.thenqueueMicrotask显式创建一个微任务queueMicrotask(() { console.log(我也是微任务); }); Promise.resolve().then(() { console.log(Promise微任务); }); // 两者都是微任务执行顺序相同浏览器渲染时机不是每次事件循环都会渲染浏览器会批量处理// 可能只触发一次重排/重绘 div.style.top 100px; div.style.left 100px; div.style.width 200px; // 而不是三次单独的重排任务分解 — 避免卡顿长时间任务可以分解让页面保持响应function processItems(items) { let i 0; function step() { // 处理一项 process(items[i]); i; if (i items.length) { // 用 setTimeout 让出主线程 setTimeout(step, 0); } } step(); } // 现代浏览器可以用 scheduler.yield() async function processItems(items) { for (const item of items) { process(item); await scheduler.yield(); // 让出主线程 } }横向对比API类型优先级使用场景setTimeout宏任务低延迟执行、轮询setInterval宏任务低定时任务慎用Promise.then微任务高异步结果处理async/await微任务高异步代码写法requestAnimationFrame宏任务中动画、游戏循环MutationObserver微任务高DOM 变化监听怎么选场景推荐延迟执行setTimeout等待 Promiseawait / Promise.then动画/游戏requestAnimationFrame批量 DOM 操作MutationObserver分解长任务setTimeout / scheduler.yield()总结概念像什么作用调用栈厨师灶台同步代码执行任务队列取餐口等待执行的异步任务宏任务普通取餐号setTimeout、setInterval微任务VIP会员卡Promise、queueMicrotask事件循环传唤员协调调用栈和任务队列同步代码 → 微任务 → 宏任务 → 渲染 → 下一轮写在最后现在你应该明白了setTimeout(fn, 0)不是马上执行要等调用栈空、微任务清空后才轮到你Promise比setTimeout先执行因为微任务优先级更高async/await只是 Promise 的语法糖本质还是异步requestAnimationFrame是做动画的正确方式别用 setInterval下次你的代码执行顺序不对先看看是微任务还是宏任务——可能就是它插队了

更多文章