JavaScript为啥是单线程运行? - Web前端
作者:98wpeu发布时间:2026-10-22分类:网页前端技术浏览:7

很多刚接触JavaScript的同学,写代码时总会疑惑“为什么setTimeout里的代码不是立刻执行?”“Promise.then和settimeout谁先执行?”这些问题背后,其实都和javascript的事件循环机制有关,想要彻底搞懂异步代码的执行顺序、解决回调地狱或者优化性能,事件循环是必须跨过去的坎,今天咱们就把事件循环拆碎了讲,从基础概念到实际场景,一步步弄明白它到底怎么运作。
得先从JavaScript的设计场景说起,它诞生是为了在浏览器里操作DOM、做交互,如果同时有两个线程改同一个dom,浏览器根本不知道听谁的,所以从根上,Javascript就被设计成**单线程**——同一时间只能执行一个任务,但问题来了:如果遇到网络请求、定时器这种耗时操作,单线程干等着岂不是会“卡壳”?比如点击按钮后发请求,要是等着服务器响应期间页面不能动,用户体验直接崩了,这时候,**异步处理机制**和事件循环就派上用场了。
“调用栈”是干啥的?同步代码咋执行?
咱们写的代码,会被拆成一个个“函数调用”,这些调用会被放进一个叫调用栈(call Stack)的地方,调用栈是“后进先出”的结构,就像堆盘子,新函数调用往上堆,执行完再往下弹。
举个例子:
function a() {
function b() {
console.log('b执行');
}
b();
console.log('a执行');
}
a();执行顺序是:先把a()压入栈,执行a里的代码时遇到b(),把b()压入栈;b执行完打印“b执行”,b弹出栈;回到a里执行console.log('a执行'),最后a弹出栈,整个过程同步代码全靠调用栈按顺序执行。
异步任务咋处理?得靠“任务队列”和“Web APIs”
但遇到异步任务(比如settimeout、fetch请求),调用栈不能干等着,否则页面就卡了,这时候JavaScript引擎会把异步任务丢给浏览器的WEB APIs(可以理解成浏览器提供的后台线程)处理,等Web APIs把异步操作完成后(比如定时器到时间、请求拿到响应),就会生成一个“任务”,丢到任务队列(Task Queue)里等着被处理。
这里得区分宏任务(Macro Task)和微任务(Micro Task)两种队列:
宏任务:可以理解成“大任务”,像
setTimeout、SetInterval、DOM渲染、script标签整体执行、requestAnimationFrame这些都属于宏任务。微任务:“小任务”,优先级比宏任务高,像
promise.then/catch/finally、MutationObserver(监听DOM变化)、queuemicrotask这些属于微任务。
事件循环的核心流程:“执行栈→任务队列”循环
事件循环(event Loop)的核心逻辑可以简化成:只要调用栈空了,就去任务队列里捞任务执行,但实际流程要分宏任务和微任务的优先级:
先把当前宏任务(比如整个script脚本)里的同步代码全塞进调用栈执行完,调用栈空了。
执行所有微任务队列里的任务(因为微任务优先级高,要一次性清完),每个微任务执行时,可能又生成新的微任务,也得接着执行,直到微任务队列为空。
微任务全处理完后,去执行一个宏任务(比如
setTimeout的回调),执行完这个宏任务里的同步代码后,再重复步骤2,处理微任务……循环往复。
举个经典例子理解顺序:
console.log('start');
setTimeout(() => {
console.log('setTimeout宏任务');
}, 0);
PRomise.resolve().then(() => {
console.log('Promise微任务');
});
console.log('end');执行步骤拆解:
第一步:调用栈执行同步代码
console.log('start')→ 打印start;遇到
setTimeout,属于宏任务,丢给Web APIs计时,时间到了丢到宏任务队列;遇到
Promise.then,属于微任务,丢到微任务队列;执行
console.log('end')→ 打印end;现在调用栈空了,先处理微任务队列:执行
Promise.then的回调 → 打印Promise微任务;微任务队列为空后,处理宏任务队列里的
setTimeout回调 → 打印setTimeout宏任务。
所以最终输出顺序是:start → end → Promise微任务 → setTimeout宏任务。
宏任务和微任务具体有哪些?
得记清楚常见的任务类型,不然遇到复杂异步代码容易懵:
宏任务(Macro Task)常见类型:
setTimeout、setInterval:定时器类;setImmediate(node.js环境);DOM渲染、页面重绘;
script标签的整体执行(可以理解成第一个宏任务);
requestanimationFrame(虽然和渲染有关,但属于宏任务,不过它的触发时机和屏幕刷新率同步)。
微任务(Micro Task)常见类型:
Promise的then、catch、finally回调(注意new Promise里的执行器是同步的,只有.then这些才是微任务);MutationObserver:监听Dom节点变化的回调;queueMicrotask:手动创建微任务的API(比如queueMicrotask(() => { ... }));Node.js里的
process.nextTick(它比普通微任务优先级更高,属于“nexttick队列”,但浏览器里没有)。
复杂场景下事件循环怎么玩?再看个多层嵌套例子
实际开发中,异步代码经常嵌套,比如Promise里套setTimeout,或者微任务里又生成微任务,看这个例子:
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');一步步分析执行流程:
执行同步代码:
console.log('1')→ 打印1;遇到
setTimeout(宏任务),丢到宏任务队列;遇到
Promise.then(微任务),丢到微任务队列;执行
console.log('6')→ 打印6;调用栈空了,处理微任务队列:执行
Promise.then的回调 → 打印4;
回调里又遇到setTimeout(宏任务),丢到宏任务队列;微任务队列处理完了,现在处理宏任务队列里的第一个宏任务(最开始的
setTimeout):
执行回调里的console.log('2')→ 打印2;
然后遇到Promise.then(微任务),丢到微任务队列;这个宏任务里的同步代码执行完了,现在处理微任务队列(因为每次宏任务执行完后要清微任务):
执行刚丢进去的Promise.then回调 → 打印3;微任务处理完,再处理宏任务队列里的下一个宏任务(步骤5里丢的
setTimeout):
执行回调 → 打印5。
所以最终输出顺序是:1 → 6 → 4 → 2 → 3 → 5。
事件循环对开发有啥实际影响?避坑和优化思路
理解事件循环不是为了背概念,而是解决实际问题:
避免微任务过多阻塞渲染
比如在微任务里做大量DOM操作或者循环计算,因为微任务要一次性执行完才会触发渲染(渲染属于宏任务),如果微任务耗时太长,页面会出现“掉帧”或者交互延迟,举个反面例子:
function loop() {
queueMicrotask(loop); // 无限创建微任务,页面直接卡死
}
loop();这种情况要拆分成宏任务,或者用requestIdleCallback(不过兼容性一般)。
合理利用微任务做“即时更新”
比如用户点击按钮后,要先更新状态再触发渲染,用微任务就很合适,因为微任务执行完后,下一个宏任务才是渲染,所以微任务里改DOM属性,能保证渲染时是最新的状态。
定时器不准的原因
setTimeout的回调不是“到时间就执行”,而是“到时间后丢到宏任务队列,等调用栈空了、微任务也空了才执行”,如果前面有大量任务,定时器实际执行时间会比预期晚,所以不能依赖setTimeout做高精度计时(比如动画),优先用requestAnimationFrame。
事件循环的关键逻辑
最后帮大家把核心逻辑串起来:
javascript单线程→同步代码走调用栈→异步任务丢给Web APIs→完成后丢到任务队列(分宏/微任务);
事件循环逻辑:调用栈空了→先清所有微任务→再执行一个宏任务→再清所有微任务→循环;
微任务优先级高于宏任务,同个宏任务里的微任务要一次性处理完,再处理下一个宏任务。
把这些逻辑吃透,再遇到异步代码执行顺序的问题,就能像“拆积木”一样一步步分析清楚,平时写代码时,也能更合理地安排异步任务,避免页面卡顿或者逻辑执行顺序出错啦~
相关推荐
- JavaScript 怎么检测数据类型?常用方法全解析 - Web前端
- 模块化封装,把逻辑打包成可复用的小工具 - Web前端
- 先搞懂「防抖(debounce)是啥? - Web前端
- 为啥处理大文件得用Stream,普通读写有啥问题? - Web前端
- 浏览器是如何一步步把代码变成网页的? - Web前端
- JS基础知识:先搞懂浅拷贝和深拷贝的核心区别 - Web前端
- 网页端OCR识别功能集成要注意哪些关键点? - Web前端
- 网页端脑图工具总卡顿?这6个性能优化技巧必须掌握 - Web前端
- 询问SEO:如何从现场迁移中恢复30%的有机流量下降? - Web前端
- 微信服务通知怎么关闭?分场景教你轻松管理通知干扰 - Web前端
- 网页前端技术排行
-
- 1[Web前端]用javascript实现默认图片替代未显示的图片 - Web前端
- 2分析Iconfont-阿里巴巴矢量常用图标库 - Web前端
- 3jQuery实例教程:制作网页中可折叠的面板 - Web前端
- 4【第六章】Foundation之按钮和下拉功能 - Web前端
- 5基于jquery的滚动条滚动固定div(附演示下载) - Web前端
- 6分享精心挑选的12款优秀jQuery Ajax分页插件和教程 - Web前端
- 7jQuery实现鼠标移到元素上动态提示消息框效果 - Web前端
- 8jQuery编写widget的一些技巧分享 - Web前端
- 9在Mac/PC上远程调试iPhone/iPad上的网页 - Web前端
- 最近发表
-
- WordPress随机显示特色图片插件:Random Post Thumbnails
- KeePass实现Chrome浏览器自动填充密码方法一
- LNMP一键包nginx 301强制跳转到https教程
- KeePass实现Chrome浏览器自动填充密码方法二
- #建站# 免费的VPS管理软件Xshell8/Xftp8中文版下载
- 使用Xshell 8连接VPS教程_电脑登录vps的方法
- WordPress评论界面添加烟花????效果
- 不同浏览器书签同步方案:坚果云+Floccus_详细使用教程
- iOS端KeePassXC客户端APP:Strongbox Password Safe
- 给WordPress评论中的Gravatar头像图片添加ALT属性


