前言
在JavaScript的世界中,同步任务和异步任务的协同工作是实现高效非阻塞编程的核心。无论是处理用户点击事件、发起网络请求,还是执行定时操作,理解它们的运行机制都是每位开发者的必修课。
同步任务
什么是同步任务
同步任务(Synchronous Task)是立即在主线程执行栈中顺序执行的代码。它们像流水线上的工人,必须等待前一个任务完成后才能开始下一个任务。
同步任务有哪些
常见的同步任务有:
- 普通函数的调用,例如console.log
- 变量的声明,赋值
- 同步的代码逻辑。例如(if,for等控制流语句)
同步任务的特点
- 确定性:同步任务会按照代码的顺序依次执行,意味着前一个任务必须全部执行完毕,后一个任务才会开始执行。
console.log("第一步:准备食材"); // 同步 console.log("第二步:开始烹饪"); // 同步 console.log("第三步:装盘上菜"); // 同步 // 输出顺序严格按照代码书写顺序执行
- 阻塞性:若有一个非常耗时的同步任务,会阻塞主线程,导致页面卡顿
console.log("开始计算"); for(let i=0; i<3e9; i++){} // 模拟耗时操作 console.log("计算完成"); // 页面会在这期间卡死
简单性:同步任务的执行顺序固定,代码从上到下依次执行,这种执行模式使得代码的逻辑更加清晰易懂。对于简单的任务,同步代码往往更加直观和易于维护。
异步任务
为什么需要异步任务
当遇到网络请求(约200ms)、文件读取(约500ms)等耗时操作时,同步模式会导致:
浏览器界面冻结
用户体验恶化
资源利用率低下
异步任务
-
定义:不立即执行的任务,会被放入任务队列(分为宏任务队列和微任务队列),等待主线程空闲时处理。
异步任务的分类
宏任务
定义:由浏览器或 Node.js 环境提供的、需要排队等待执行的任务。
常见场景:
- setTimeout / setInterval(定时器)
- DOM 事件回调(如点击、滚动)
- I/O 操作(如文件读写)
- requestAnimationFrame(浏览器)
- setImmediate(Node.js)
执行机制:
每次事件循环(Event Loop)会从宏任务队列中取出一个任务执行。
执行完毕后,检查微任务队列并清空所有微任务,再进行下一轮循环。
微任务
定义:优先级高于宏任务的任务,通常与 Promise 相关。
常见场景:
Promise.then() /Promise.catch()
MutationOberver(监听 DOM 变化)
queueMincrotask (显式添加微任务)
process.nextTick() Node.js,优先级最高)
执行机制:
在当前同步代码执行完毕后,立即清空所有微任务。
如果在微任务中又产生了新的微任务,这些新微任务也会被立即执行,直到队列为空。
经典异步场景
console.log("同步任务 1");setTimeout(() => {console.log("宏任务(setTimeout)");
}, 0);Promise.resolve().then(() => {console.log("微任务(Promise)");
});console.log("同步任务 2");// 输出顺序:
// 同步任务 1 → 同步任务 2 → 微任务(Promise) → 宏任务(setTimeout)
事件循环机制
核心机制:
执行同步代码:优先执行当前执行栈(Call Stack)中的所有同步任务。
检查微任务队列:执行所有微任务(如
Promise.then
、MutationObserver
),直到队列清空。渲染页面(浏览器环境):更新 DOM、执行
requestAnimationFrame
回调等(仅浏览器)。执行一个宏任务:从宏任务队列(如
setTimeout
、DOM 事件
)中取出一个任务执行。循环往复:重复上述步骤。
从代码看执行顺序
混合任务示例:
setTimeout(() => console.log("宏任务1"), 0);Promise.resolve().then(() => {console.log("微任务1");setTimeout(() => console.log("宏任务2"), 0);});Promise.resolve().then(() => console.log("微任务2"));console.log("同步任务");/* 输出顺序:同步任务 → 微任务1 → 微任务2 → 宏任务1 → 宏任务2 */
Node.js的特殊情况:
setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("Promise"));// 输出顺序:
// nextTick → Promise → setTimeout → setImmediate
微任务中产生新的微任务:
Promise.resolve().then(() => {console.log("微任务 1");Promise.resolve().then(() => {console.log("微任务 2");});
});// 输出顺序:微任务 1 → 微任务 2
微任务阻塞宏任务 :
setTimeout(() => {console.log("宏任务");
}, 0);Promise.resolve().then(() => {while (true) {} // 无限循环,阻塞后续所有任务
});// 永远不会输出 "宏任务"
常见问题
setTimeout(fn,0)真的0秒执行吗?
- 实际会有至少4ms的延迟(HTML5规范)
- 受事件循环状态影响
微任务会阻塞渲染吗?
document.body.style.backgroundColor = 'red'; Promise.resolve().then(() => {for(let i=0; i<3e9; i++){} // 页面保持红色无法更新 }); document.body.style.backgroundColor = 'blue';
如何避免异步陷阱?
// 错误示例 for(var i=0; i<5; i++){setTimeout(() => console.log(i), 0); // 输出5个5 }// 正确方案 for(let i=0; i<5; i++){ // 使用let块级作用域setTimeout(() => console.log(i), 0); // 0-4 }