JS执行机制灵魂拷问:为什么你的setTimeout总是不准时?答案颠覆认知!

一、JS为什么需要"排队"?

JS 是单线程语言(想象成只有一个收银台的超市),所有任务必须排队执行。但有些任务很耗时(比如网络请求),如果傻等会导致页面卡死,于是有了这样的分工:

// 同步任务:立即执行的普通代码
console.log("我要第一个执行"); 

// 异步任务:需要等结果的任务
setTimeout(() => {
  console.log("我是迟到的小龙虾外卖");
}, 1000);

console.log("我要第二个执行");

输出顺序:

我要第一个执行
我要第二个执行
我是迟到的小龙虾外卖
流程图解:
任务完成
主线程
执行同步任务
遇到异步任务?
丢给WebAPI处理
任务队列
事件循环检查任务队列

二、事件循环(Event Loop)是怎么干活的?

事件循环就像个监工,核心工作流程分三步:

  1. 执行栈:同步代码直接在这里执行(像叠盘子)
  2. 任务队列:异步任务完成后,回调函数在这里排队(像待取的快递)
  3. 循环机制:执行栈空了就去队列取任务来执行
经典面试题解析:
console.log("脚本启动");

setTimeout(() => {
  console.log("定时器回调");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise回调");
});

console.log("脚本结束");

输出顺序:

脚本启动
脚本结束
Promise回调
定时器回调
关键差异:
宏任务(MacroTask)微任务(MicroTask)
代表setTimeout/setIntervalPromise.then
优先级
插入位置任务队列末尾当前执行栈尾部

三、为什么微任务能"插队"?

微任务就像 VIP 通道,每次执行栈清空后,必须立即清空所有微任务才会处理宏任务。

代码演示插队现场:
console.log("开始");

// 宏任务(普通顾客)
setTimeout(() => {
  console.log("宏任务执行");
}, 0);

// 微任务(VIP 顾客)
Promise.resolve().then(() => {
  console.log("微任务1");
}).then(() => {
  console.log("微任务2");
});

console.log("结束");

输出顺序:

开始
结束
微任务1
微任务2
宏任务执行
执行流程示意图:
下一轮循环
主线程
处理微任务队列
需要渲染?
执行渲染
处理宏任务队列
事件循环

四、解剖 Promise 的执行顺序

Promise 的构造器是同步执行的,但 then/catch 是异步微任务

分步解析代码:
console.log("1. 同步代码开始");

new Promise((resolve) => {
  console.log("2. Promise构造器立即执行");
  resolve();
}).then(() => {
  console.log("4. 第一个then回调");
}).then(() => {
  console.log("5. 第二个then回调");
});

setTimeout(() => {
  console.log("6. 定时器回调");
}, 0);

console.log("3. 同步代码结束");

输出顺序:

1. 同步代码开始
2. Promise构造器立即执行
3. 同步代码结束
4. 第一个then回调
5. 第二个then回调
6. 定时器回调
关键点:
  1. new Promise 内部的代码是同步执行的
  2. 每个 then() 都会创建新的微任务
  3. 微任务链会一次性执行完毕

五、async/await 的本质

async 函数本质上是 Promise 的语法糖,await 会暂停代码执行(但不会阻塞主线程)。

对比代码:
// 传统 Promise 写法
function oldSchool() {
  Promise.resolve().then(() => {
    console.log("Promise结果");
  });
}

// async/await 写法
async function modern() {
  await Promise.resolve(); 
  console.log("async结果");
}

oldSchool();   // 输出顺序:Promise结果
modern();      // 输出顺序:async结果
执行流程解析:
async function demo() {
  console.log("1. await之前");
  await Promise.resolve();
  console.log("3. await之后");
}

console.log("0. 外层同步");
demo();
console.log("2. 外层结束");

// 输出顺序:
// 0. 外层同步
// 1. await之前
// 2. 外层结束
// 3. await之后
关键结论:
  • await 后面的代码相当于 then() 回调
  • 整个 async 函数会返回 Promise 对象

六、完整执行机制流程图解

宏任务
微任务
执行栈
执行同步代码
遇到异步任务?
交给WebAPI计时
交给WebAPI处理
宏任务队列
微任务队列
执行栈清空
事件循环开始
检查微任务队列
微任务队列空?
执行所有微任务
需要渲染?
执行渲染
取一个宏任务
执行宏任务回调
  1. 所有同步代码进入执行栈
  2. 遇到异步任务:
    • 宏任务:(如setTimeout/setInterval)交给 WebAPI 计时,完成后回调进入宏任务队列
    • 微任务:(如Promise)完成后回调进入微任务队列
    • 完成后回调分别进入对应队列
  3. 执行栈清空后:
    • 检查微任务队列,全部执行完毕
    • 执行渲染(如果需要)
    • 取一个宏任务执行
  4. 循环往复:
    • 每轮循环只执行一个宏任务
    • 每个宏任务执行后都会重新检查微任务队列
    • 渲染操作通常每秒60次(16.7ms/次),但会受任务阻塞影响
综合练习题:
console.log("start");

setTimeout(() => {
  console.log("timeout1");
  Promise.resolve().then(() => {
    console.log("promise1");
  });
}, 0);

setTimeout(() => {
  console.log("timeout2");
  Promise.resolve().then(() => {
    console.log("promise2");
  });
}, 0);

Promise.resolve().then(() => {
  console.log("promise3");
});

console.log("end");

正确答案:

start
end
promise3
timeout1
promise1
timeout2
promise2

七、核心要点总结

  1. 执行顺序优先级
    同步代码 > 微任务 > 宏任务

  2. 常见类型分类

    // 宏任务
    setTimeout, setInterval, I/O, UI渲染
    
    // 微任务
    Promise.then, process.nextTick, MutationObserver
    
  3. 避坑指南

    • 避免在微任务中创建大量微任务导致死循环
    • 时间参数为0的定时器不代表立即执行
    • 微任务队列优先于页面渲染执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值