JavaScript 是怎么运行的?
🎯 引言
如果你是刚入门的前端小白,理解 JavaScript 是怎么运行的,将帮助你写出更高效的代码,避免常见的异步陷阱。通过本文,你会明白为什么 JavaScript 是单线程语言,掌握同步与异步的差别,知道调用栈是如何让代码一行行执行,并且了解浏览器里的事件循环机制,从而更好地调试和优化你的前端程序。
🚦 JavaScript 是单线程语言,为什么?
JavaScript 从诞生开始就设计成 单线程 语言。简单来说,单线程就是指 JavaScript 拥有且只有一个 执行线程,这意味着:同一时刻只能做一件事情。
为什么要设计成单线程呢?这背后有几个原因:
- 历史原因:JavaScript 最初是为网页交互设计的,目的是让网页能响应用户操作。单线程设计简单,避免了多线程编程中常见的复杂问题,比如“线程安全”和“资源竞用”。
- DOM 操作安全:网页里的 DOM(文档对象模型)只有一个,单线程能保证不会出现多个线程同时修改 DOM 导致页面混乱的情况。
- 易于开发:单线程模型让开发者不用担心线程同步问题,降低了编程复杂度。
🎭 同步与异步的区别
JavaScript 代码执行有两种方式:同步 和 异步。
- 同步:代码一行接一行执行,前一行执行完,后一行才能执行,像排队买票,必须等前面的人买完才能轮到你。
- 异步:代码不会阻塞下一行执行,某些操作会被放到后台处理,等结果好了再回来通知你,像你叫外卖,等外卖送来之前可以继续干别的事儿。
同步示例
console.log('开始');
console.log('处理中');
console.log('结束');
输出顺序严格按照代码顺序:
开始
处理中
结束
异步示例
console.log('开始');
setTimeout(() => {
console.log('定时器执行');
}, 1000);
console.log('结束');
输出顺序是:
开始
结束
定时器执行
为什么“结束”先打印?因为设置的定时器是异步操作,不会阻塞主线程,1秒后才执行回调。
🧱 什么是调用栈?代码是怎么一行一行执行的?
调用栈(Call Stack)是 JavaScript 管理代码执行顺序的工具。
当你写的代码运行时,JavaScript 引擎会把每个执行的函数压入调用栈,等函数执行完毕后,会把它弹出栈外,继续执行下一个函数。
举个例子
function first() {
console.log('first 函数开始');
second();
console.log('first 函数结束');
}
function second() {
console.log('second 函数执行');
}
first();
执行流程:
- 调用
first(),first函数入栈 - 打印
first 函数开始 - 调用
second(),second函数入栈 - 打印
second 函数执行 second()执行完毕,出栈- 打印
first 函数结束 first()执行完毕,出栈
调用栈保证了代码执行的顺序。
🔄 浏览器里的事件循环机制,宏任务微任务
JavaScript 的单线程特性限制它一次只能做一件事,但很多操作是异步的,比如网络请求、定时器、用户点击等。浏览器为了让异步代码顺利执行,引入了 事件循环(Event Loop) 机制。
事件循环简单理解
事件循环不断监视调用栈和任务队列:
- 如果调用栈空了,从任务队列取出第一个任务放入调用栈执行。
- 这样保证异步任务能在合适时机执行。
宏任务(Macro Task)和微任务(Micro Task)
浏览器中的任务分两类:
- 宏任务:大任务,比如
setTimeout、setInterval、UI 渲染、网络请求回调等。 - 微任务:小任务,比如
Promise.then、MutationObserver等。
事件循环执行顺序:
- 执行一个宏任务(比如整体代码脚本)
- 执行所有微任务队列中的任务(直到清空)
- 渲染更新页面
- 进入下一轮宏任务
举例说明
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
输出顺序:
script start
script end
promise1
promise2
setTimeout
为什么?
- 整个脚本是宏任务,开始执行,先打印
script start - 遇到
setTimeout,定时器回调加入宏任务队列 - 遇到
Promise.then,回调加入微任务队列 - 打印
script end - 宏任务执行完,调用栈清空,开始执行微任务队列,依次打印
promise1和promise2 - 微任务执行完毕,开始执行下一宏任务,打印
setTimeout
🧾 小节总结
- JavaScript 是单线程语言,保证了 DOM 操作安全和编程简单。
- 同步代码按顺序执行,异步代码不会阻塞主线程,让页面更流畅。
- 调用栈管理函数执行顺序,保证代码一行行地执行。
- 事件循环机制管理异步任务执行,宏任务和微任务分别排队,微任务优先执行。
❓ 知识问答(Q&A)
Q:为什么 JavaScript 只能单线程执行?
A:为了避免同时操作 DOM 导致混乱,并且简化编程模型,JavaScript 设计为单线程语言。
Q:异步和同步有什么区别?
A:同步代码一行行执行,阻塞后续代码;异步代码不会阻塞主线程,任务完成后再执行回调。
Q:调用栈是什么?
A:调用栈是一个栈结构,存放当前正在执行的函数,保证函数执行顺序。
Q:事件循环机制中,微任务和宏任务有什么区别?
A:宏任务包括整体代码脚本、定时器回调等;微任务包括 Promise 回调等,微任务优先执行。
🧪 小练习
- 编写一段代码,演示同步和异步执行顺序。
console.log('同步开始');
setTimeout(() => {
console.log('异步定时器');
}, 0);
console.log('同步结束');
请在上面代码基础上添加 Promise 的异步回调,观察输出顺序。
- 模拟调用栈执行顺序,写两个函数互相调用,打印调用栈顺序。
function foo() {
console.log('foo start');
bar();
console.log('foo end');
}
function bar() {
console.log('bar');
}
foo();
请尝试添加第三个函数,观察调用栈变化。
🎉 恭喜你已经掌握 JavaScript 单线程特性、同步异步区别、调用栈和事件循环机制技能啦!掌握这些运行原理,让你写的代码更稳定、性能更好!
