Js Advanced

JavaScript 是怎么运行的?

深入讲解 JavaScript 的运行机制,包括单线程特性、同步异步区别、调用栈、事件循环及任务队列。

🎯 引言

如果你是刚入门的前端小白,理解 JavaScript 是怎么运行的,将帮助你写出更高效的代码,避免常见的异步陷阱。通过本文,你会明白为什么 JavaScript 是单线程语言,掌握同步与异步的差别,知道调用栈是如何让代码一行行执行,并且了解浏览器里的事件循环机制,从而更好地调试和优化你的前端程序。


🚦 JavaScript 是单线程语言,为什么?

JavaScript 从诞生开始就设计成 单线程 语言。简单来说,单线程就是指 JavaScript 拥有且只有一个 执行线程,这意味着:同一时刻只能做一件事情。

为什么要设计成单线程呢?这背后有几个原因:

  • 历史原因:JavaScript 最初是为网页交互设计的,目的是让网页能响应用户操作。单线程设计简单,避免了多线程编程中常见的复杂问题,比如“线程安全”和“资源竞用”。
  • DOM 操作安全:网页里的 DOM(文档对象模型)只有一个,单线程能保证不会出现多个线程同时修改 DOM 导致页面混乱的情况。
  • 易于开发:单线程模型让开发者不用担心线程同步问题,降低了编程复杂度。
你可以把 JavaScript 想象成一个厨师,他一次只能处理一个菜品(任务),不能同时做两道菜。虽然效率看起来受限,但避免了厨房混乱。

🎭 同步与异步的区别

JavaScript 代码执行有两种方式:同步异步

  • 同步:代码一行接一行执行,前一行执行完,后一行才能执行,像排队买票,必须等前面的人买完才能轮到你。
  • 异步:代码不会阻塞下一行执行,某些操作会被放到后台处理,等结果好了再回来通知你,像你叫外卖,等外卖送来之前可以继续干别的事儿。

同步示例

console.log('开始');
console.log('处理中');
console.log('结束');

输出顺序严格按照代码顺序:

开始
处理中
结束

异步示例

console.log('开始');

setTimeout(() => {
    console.log('定时器执行');
}, 1000);

console.log('结束');

输出顺序是:

开始
结束
定时器执行

为什么“结束”先打印?因为设置的定时器是异步操作,不会阻塞主线程,1秒后才执行回调。

异步机制让 JavaScript 不会被慢操作卡住,用户体验更流畅。

🧱 什么是调用栈?代码是怎么一行一行执行的?

调用栈(Call Stack)是 JavaScript 管理代码执行顺序的工具。

当你写的代码运行时,JavaScript 引擎会把每个执行的函数压入调用栈,等函数执行完毕后,会把它弹出栈外,继续执行下一个函数。

举个例子

function first() {
    console.log('first 函数开始');
    second();
    console.log('first 函数结束');
}

function second() {
    console.log('second 函数执行');
}

first();

执行流程:

  1. 调用 first()first 函数入栈
  2. 打印 first 函数开始
  3. 调用 second()second 函数入栈
  4. 打印 second 函数执行
  5. second() 执行完毕,出栈
  6. 打印 first 函数结束
  7. first() 执行完毕,出栈

调用栈保证了代码执行的顺序。

调用栈就像你做事情的清单:先把任务写入清单,做完一个划掉,再做下一个。

🔄 浏览器里的事件循环机制,宏任务微任务

JavaScript 的单线程特性限制它一次只能做一件事,但很多操作是异步的,比如网络请求、定时器、用户点击等。浏览器为了让异步代码顺利执行,引入了 事件循环(Event Loop) 机制。

事件循环简单理解

事件循环不断监视调用栈和任务队列:

  • 如果调用栈空了,从任务队列取出第一个任务放入调用栈执行。
  • 这样保证异步任务能在合适时机执行。

宏任务(Macro Task)和微任务(Micro Task)

浏览器中的任务分两类:

  • 宏任务:大任务,比如 setTimeoutsetInterval、UI 渲染、网络请求回调等。
  • 微任务:小任务,比如 Promise.thenMutationObserver 等。

事件循环执行顺序:

  1. 执行一个宏任务(比如整体代码脚本)
  2. 执行所有微任务队列中的任务(直到清空)
  3. 渲染更新页面
  4. 进入下一轮宏任务

举例说明

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
  • 宏任务执行完,调用栈清空,开始执行微任务队列,依次打印 promise1promise2
  • 微任务执行完毕,开始执行下一宏任务,打印 setTimeout
微任务优先级比宏任务高,微任务会在当前宏任务结束后立即执行。

🧾 小节总结

  • JavaScript 是单线程语言,保证了 DOM 操作安全和编程简单。
  • 同步代码按顺序执行,异步代码不会阻塞主线程,让页面更流畅。
  • 调用栈管理函数执行顺序,保证代码一行行地执行。
  • 事件循环机制管理异步任务执行,宏任务和微任务分别排队,微任务优先执行。

❓ 知识问答(Q&A)

Q:为什么 JavaScript 只能单线程执行?
A:为了避免同时操作 DOM 导致混乱,并且简化编程模型,JavaScript 设计为单线程语言。

Q:异步和同步有什么区别?
A:同步代码一行行执行,阻塞后续代码;异步代码不会阻塞主线程,任务完成后再执行回调。

Q:调用栈是什么?
A:调用栈是一个栈结构,存放当前正在执行的函数,保证函数执行顺序。

Q:事件循环机制中,微任务和宏任务有什么区别?
A:宏任务包括整体代码脚本、定时器回调等;微任务包括 Promise 回调等,微任务优先执行。


🧪 小练习

  1. 编写一段代码,演示同步和异步执行顺序。
console.log('同步开始');

setTimeout(() => {
    console.log('异步定时器');
}, 0);

console.log('同步结束');

请在上面代码基础上添加 Promise 的异步回调,观察输出顺序。

  1. 模拟调用栈执行顺序,写两个函数互相调用,打印调用栈顺序。
function foo() {
    console.log('foo start');
    bar();
    console.log('foo end');
}

function bar() {
    console.log('bar');
}

foo();

请尝试添加第三个函数,观察调用栈变化。


🎉 恭喜你已经掌握 JavaScript 单线程特性、同步异步区别、调用栈和事件循环机制技能啦!掌握这些运行原理,让你写的代码更稳定、性能更好!