为什么 JavaScript 代码执行顺序错乱?

文章目录:

  1. JavaScript 代码执行顺序概述
    • 1.1 JavaScript 是单线程的
    • 1.2 异步编程与事件循环
  2. 常见问题:为什么代码执行顺序错乱?
    • 2.1 同步代码与异步代码的执行顺序
    • 2.2 setTimeoutPromise 的执行顺序
    • 2.3 事件循环机制(Event Loop)
  3. 实际项目代码示例
    • 3.1 示例 1:setTimeout 异步执行
    • 3.2 示例 2:Promise 与异步操作
    • 3.3 示例 3:async/await 中的执行顺序
  4. 如何正确理解 JavaScript 的执行顺序
    • 4.1 理解执行栈和任务队列
    • 4.2 微任务和宏任务的区别
  5. 总结

1. JavaScript 代码执行顺序概述

1.1 JavaScript 是单线程的

JavaScript 是一种单线程语言,也就是说,它一次只能执行一个任务。这是 JavaScript 执行顺序错乱的根本原因之一。虽然 JavaScript 是单线程的,但它通过 事件循环(Event Loop)异步编程,能够在执行一个任务时同时等待其他任务完成,避免阻塞主线程。

1.2 异步编程与事件循环

在 JavaScript 中,异步编程是通过 setTimeoutPromiseasync/await 等方式实现的。这些操作不会立即执行,而是被放入 任务队列 中,等待主线程空闲时执行。JavaScript 使用 事件循环 来管理执行顺序:当主线程完成当前任务时,事件循环会检查任务队列中的异步任务,按顺序执行它们。


2. 常见问题:为什么代码执行顺序错乱?

2.1 同步代码与异步代码的执行顺序

同步代码是按照书写顺序一行一行执行的,而异步代码需要等待特定事件的发生(例如,定时器到期、网络请求返回等)。这就导致了我们通常会遇到 异步操作执行顺序错乱 的情况。

示例:
console.log("开始");

setTimeout(() => {
  console.log("异步任务");
}, 0);

console.log("结束");

输出顺序:

开始
结束
异步任务

解释:

  • "开始""结束" 是同步代码,直接在栈中执行。
  • setTimeout 是异步操作,它的回调函数被放入 任务队列 中,等待事件循环将它从队列中取出并执行。

2.2 setTimeoutPromise 的执行顺序

setTimeoutPromise 的回调都可以异步执行,但它们的执行顺序是不同的。setTimeout 的回调被放入 宏任务队列,而 Promise 的回调被放入 微任务队列。微任务的优先级高于宏任务,因此会先执行 Promise 的回调。

示例:
console.log("开始");

setTimeout(() => {
  console.log("宏任务 - setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("微任务 - Promise");
});

console.log("结束");

输出顺序:

开始
结束
微任务 - Promise
宏任务 - setTimeout

解释:

  • "开始""结束" 是同步代码,直接执行。
  • Promise 的回调被放入微任务队列,setTimeout 的回调被放入宏任务队列。
  • 事件循环优先执行微任务(Promise),然后才执行宏任务(setTimeout)。

2.3 事件循环机制(Event Loop)

事件循环是 JavaScript 处理异步任务的机制。当执行栈为空时,事件循环会从任务队列中获取任务并执行。任务队列有两个主要部分:

  • 宏任务队列(Macro Task Queue):例如 setTimeoutsetIntervalI/O 等。
  • 微任务队列(Micro Task Queue):例如 PromiseMutationObserver 等。

微任务的优先级高于宏任务,因此它们会在宏任务之前执行。


3. 实际项目代码示例

3.1 示例 1:setTimeout 异步执行

console.log("开始");

setTimeout(() => {
  console.log("宏任务 1");
}, 1000);

setTimeout(() => {
  console.log("宏任务 2");
}, 0);

console.log("结束");

输出顺序:

开始
结束
宏任务 2
宏任务 1

解释:

  • "开始""结束" 是同步代码。
  • 第一个 setTimeout(延迟 1000 毫秒)被放入宏任务队列。
  • 第二个 setTimeout(延迟 0 毫秒)仍然被放入宏任务队列,但它的回调会在主线程空闲后执行,因此它在 "结束" 后执行。

3.2 示例 2:Promise 与异步操作

console.log("开始");

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

setTimeout(() => {
  console.log("宏任务 1");
}, 0);

console.log("结束");

输出顺序:

开始
结束
微任务 1
微任务 2
宏任务 1

解释:

  • "开始""结束" 是同步代码。
  • Promisethen 被放入微任务队列。
  • 事件循环会在同步代码执行完后优先执行微任务(微任务 1微任务 2),然后执行宏任务(宏任务 1)。

3.3 示例 3:async/await 中的执行顺序

async function asyncFunction() {
  console.log("开始");

  await Promise.resolve(); // 等待 Promise 执行
  console.log("等待完成");

  setTimeout(() => {
    console.log("宏任务");
  }, 0);

  console.log("结束");
}

asyncFunction();

输出顺序:

开始
结束
等待完成
宏任务

解释:

  • async 函数中的代码会立即执行,直到遇到 await
  • await 会暂停函数执行,等待 Promise 解决,并将控制权交还给事件循环。
  • 同步代码(console.log("开始")console.log("结束"))会先执行。
  • Promise 会被放入微任务队列,setTimeout 会被放入宏任务队列。

4. 如何正确理解 JavaScript 的执行顺序

4.1 理解执行栈和任务队列

JavaScript 的执行顺序由以下部分组成:

  • 执行栈(Call Stack):用来执行同步代码。
  • 宏任务队列(Macro Task Queue):用来存放 setTimeoutsetInterval 等宏任务。
  • 微任务队列(Micro Task Queue):用来存放 PromiseMutationObserver 等微任务。

当执行栈为空时,事件循环会检查微任务队列并执行其中的任务。如果微任务队列为空,才会执行宏任务队列中的任务。

4.2 微任务和宏任务的区别

  • 微任务:例如 PromiseMutationObserver,它们的执行优先级高于宏任务,在事件循环中微任务队列总是先于宏任务队列执行。
  • 宏任务:例如 setTimeoutsetInterval、DOM 操作,通常会在微任务执行完毕后执行。

5. 总结

JavaScript 的代码执行顺序可能让初学者感到困惑,尤其是在涉及异步操作时。理解 事件循环执行栈任务队列 的机制,能帮助你更好地掌握代码执行顺序。异步代码的执行顺序是由事件循环中的微任务和宏任务队列决定的,微任务总是在宏任务之前执行。通过实际项目中的代码示例,我们可以清楚地看到同步与异步代码执行的不同以及事件循环的影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂的沙粒

您的鼓励是我创作最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值