LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

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

zhenglin
2026年4月8日 16:58 本文热度 28

导读

你有没有遇到过这种情况:代码里写了 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

任务队列 — 取餐口

异步代码放哪儿?

当遇到 setTimeoutPromise事件回调 这些异步任务时,它们不会马上执行,而是被放到任务队列里。

就像点完单,服务员把单子放到取餐口,等叫号再去取。

事件循环的运行机制

代码高亮:

┌─────────────────────┐

│       调用栈         │  ← 正在执行

│   (Call Stack)       │

└─────────────────────┘

          ↓

┌─────────────────────┐

│      任务队列        │  ← 等待执行

│   (Task Queue)       │

└─────────────────────┘

          ↓

    事件循环 (Event Loop)

    "栈空了?好,取下一个"

事件循环的规则

  1. 首先执行调用栈里的所有同步代码
  2. 调用栈清空后,去任务队列取一个任务执行
  3. 完成后回到步骤1

console.log('1');


setTimeout(() => {

  console.log('2');

}, 0);


console.log('3');


// 输出:1 → 3 → 2

// 因为 setTimeout 的回调在任务队列,要等调用栈空才能执行

微任务 vs 宏任务 — VIP和普通号

两种不同的"队"

任务队列其实分两种:

类型例子优先级
宏任务(Macrotask)setTimeoutsetIntervalI/OUI渲染
微任务(Microtask)Promise.then()回调、MutationObserverqueueMicrotask

就像餐厅里:

  • 宏任务 = 普通取餐号,要排队

  • 微任务 = 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     ← 所有微任务完成后,才执行宏任务

嵌套的 Promise

代码高亮:

Promise.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 → 3

await 后面那行代码去哪了?

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 函数总是返回一个 Promise

代码高亮:

async 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 — 动画的正确姿势

为什么不用 setInterval?

setInterval 不保证什么时候执行,也不保证每次间隔精确:

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.then

queueMicrotask 显式创建一个微任务:

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) 不是马上执行,要等调用栈空、微任务清空后才轮到你

  • PromisesetTimeout 先执行,因为微任务优先级更高

  • async/await 只是 Promise 的语法糖,本质还是异步

  • requestAnimationFrame 是做动画的正确方式,别用 setInterval

下次你的代码执行顺序不对,先看看是微任务还是宏任务——可能就是它插队了。



该文章在 2026/4/8 16:58:44 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号