3252 字
16 分钟

事件循环

欢迎来到我的博客!#

使用 Markdown 格式编写。

今天学到了什么#

image-20260424194054259

异步任务(Asynchronous Task)详解#

异步任务是指在 JavaScript 中不会立即执行完成,需要等待一段时间才能返回结果的操作。与同步任务不同,异步任务不会阻塞主线程,而是在后台处理,完成后通过回调函数或 Promise 通知主线程。

1. 异步任务的定义#

  • 同步任务:立即执行的任务,会阻塞主线程直到完成。例如:

    const sum = 1 + 2; // 同步任务,立即执行
    console.log(sum); // 同步任务,立即执行
  • 异步任务:不会立即完成的任务,执行时会释放主线程,完成后再通知主线程。例如:

    setTimeout(() => console.log('延迟执行'), 1000); // 异步任务,1秒后执行
    fetch('/api/data') // 异步任务,等待网络响应
    .then(res => res.json());

2. 常见的异步任务类型#

类型示例特点
网络请求fetch()axios.get()等待服务器响应
定时器setTimeout()setInterval()延迟执行
文件操作fs.readFile()(Node.js)读写文件系统
DOM 事件element.addEventListener('click', callback)等待用户交互
Promisenew Promise((resolve) => setTimeout(resolve, 1000))自定义异步操作
async/awaitawait fetch('/api/data')基于 Promise 的语法糖

3. 异步任务的执行机制#

JavaScript 通过事件循环(Event Loop)任务队列(Task Queue) 来处理异步任务:

  1. 执行栈:主线程执行同步任务,形成执行栈。
  2. 任务队列:异步任务完成后,将回调函数放入任务队列。
  3. 事件循环:执行栈为空时,从任务队列中取出任务执行。

执行流程示例

image-20260424195626357

console.log('1'); // 同步任务,立即执行
setTimeout(() => console.log('2'), 0); // 异步任务,放入任务队列
console.log('3'); // 同步任务,立即执行
// 执行结果:1 → 3 → 2

4. 异步任务的分类#

在现代 JavaScript 中,异步任务分为两类:

  • 宏任务(Macro Task)

    • 包括:setTimeoutsetIntervalI/O 操作、DOM 事件setImmediate(Node.js)
    • 执行顺序:每个宏任务执行完后,会检查并执行所有微任务
  • 微任务(Micro Task)

    • 包括:Promise.then()async/awaitprocess.nextTick(Node.js)、queueMicrotask
    • 执行顺序:当前宏任务执行完后,立即执行所有微任务

5. 为什么需要异步任务?#

  • 避免阻塞主线程:如果所有操作都是同步的,网络请求或文件读写等耗时操作会导致页面卡顿,用户体验差。
  • 提高性能:主线程可以在等待异步任务完成时处理其他工作,充分利用系统资源。
  • 处理并发操作:虽然 JavaScript 是单线程的,但通过异步任务可以模拟并发效果,同时处理多个操作。

6. 异步任务的处理方式#

  • 回调函数:早期的异步处理方式,容易导致回调地狱(Callback Hell)。

    fs.readFile('file1.txt', (err, data1) => {
    fs.readFile('file2.txt', (err, data2) => {
    fs.readFile('file3.txt', (err, data3) => {
    // 嵌套多层,代码难以维护
    });
    });
    });
  • Promise:解决回调地狱,提供链式调用。

    readFile('file1.txt')
    .then(data1 => readFile('file2.txt'))
    .then(data2 => readFile('file3.txt'))
    .then(data3 => console.log(data3))
    .catch(err => console.error(err));
  • async/await:Promise 的语法糖,使代码更像同步代码。

    async function readFiles() {
    try {
    const data1 = await readFile('file1.txt');
    const data2 = await readFile('file2.txt');
    const data3 = await readFile('file3.txt');
    console.log(data3);
    } catch (err) {
    console.error(err);
    }
    }

总结:异步任务是 JavaScript 中处理耗时操作的核心机制,通过事件循环和任务队列实现非阻塞执行,提高了代码的执行效率和用户体验。现代 JavaScript 提供了 Promise 和 async/await 等语法糖,使异步代码的编写和阅读更加简洁直观。

流程图#

image-20260424200248503浏览器是多线程的,由浏览器来执行异步任务

执行栈与异步任务结果的处理#

当执行栈中的任务需要使用异步任务的结果时,确实需要特殊处理,否则会出现获取不到结果或报错的情况。但 JavaScript 通过回调函数、Promise 和 async/await 等机制,确保了异步任务的结果能够被正确处理,同时不会阻塞执行栈。

1. 错误的处理方式:直接使用异步任务的结果#

如果直接在执行栈中尝试使用异步任务的结果,会导致获取到 undefined 或报错,因为异步任务还未完成:

// 错误示例
function getAsyncData() {
let result;
setTimeout(() => {
result = '异步数据';
}, 1000);
return result; // 此时 result 还是 undefined
}
const data = getAsyncData();
console.log(data); // 输出 undefined,而不是 '异步数据'

原因setTimeout 是异步任务,会被放入任务队列,而 return result 是同步任务,会立即执行,此时 result 还未被赋值。

2. 正确的处理方式#

JavaScript 提供了多种方式来处理异步任务的结果,确保在结果可用时再使用:

方式一:回调函数#
function getAsyncData(callback) {
setTimeout(() => {
const result = '异步数据';
callback(result); // 异步任务完成后调用回调函数
}, 1000);
}
getAsyncData((data) => {
console.log(data); // 1秒后输出 '异步数据'
});
方式二:Promise#
function getAsyncData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('异步数据');
}, 1000);
});
}
getAsyncData().then((data) => {
console.log(data); // 1秒后输出 '异步数据'
});
方式三:async/await#
function getAsyncData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('异步数据');
}, 1000);
});
}
async function main() {
const data = await getAsyncData(); // 暂停执行,等待异步任务完成
console.log(data); // 1秒后输出 '异步数据'
}
main();

3. 为什么不会阻塞?#

JavaScript 的异步机制确保了即使需要等待异步任务的结果,也不会阻塞执行栈:

  • 事件循环:异步任务会被放入任务队列,执行栈在遇到异步任务时会继续执行其他同步任务,不会等待。
  • 回调机制:异步任务完成后,通过回调函数通知执行栈,此时执行栈已经处理完了其他任务。
  • await 暂停:使用 await 时,虽然函数会暂停执行,但会释放主线程,允许其他代码执行。

4. 实际应用中的最佳实践#

  • 使用 Promise 或 async/await:现代 JavaScript 推荐使用这些方式处理异步任务,代码更清晰。
  • 避免回调地狱:使用 Promise 链式调用或 async/await 避免多层嵌套的回调。
  • 错误处理:使用 try/catch(async/await)或 .catch()(Promise)捕获异步操作中的错误。
  • 并行处理:对于多个独立的异步任务,使用 Promise.all() 并行执行,提高效率。

总结:当执行栈中的任务需要使用异步任务的结果时,不能直接获取,需要通过回调函数、Promise 或 async/await 等机制来处理。这些机制确保了异步任务的结果能够被正确获取,同时不会阻塞执行栈,保证了 JavaScript 的单线程执行效率。

例子1#

你的理解完全正确!#

axios.get('/api/list') 确实是一个异步任务,而 console.log(list) 是同步任务,会立即执行。让我详细解释这个执行过程:

执行流程分析#

  1. 同步代码执行

    • 当代码执行到第9行时,axios.get('/api/list') 被调用,发起网络请求。
    • 由于这是一个异步操作,JavaScript 引擎会将其放入任务队列(具体来说是宏任务队列)。
    • 同时,axios.get() 会立即返回一个 Promise 对象,此时 Promise 处于 pending 状态。
  2. 链式调用

    • 接着调用 .then() 方法,注册一个回调函数。这个回调函数会在 Promise 状态变为 fulfilled 时执行。
    • 整个 axios.get('/api/list').then(...) 表达式的结果仍然是一个 Promise 对象(因为 .then() 方法返回一个新的 Promise)。
  3. 变量赋值

    • 将这个 Promise 对象赋值给 list 变量。
  4. 同步执行

    • 执行第13行的 console.log(list);,这是一个同步任务,会立即执行。
    • 此时网络请求还未完成,Promise 仍处于 pending 状态,所以会打印出 Promise {<pending>}
  5. 异步回调执行

    • 当网络请求完成后,axios 会将 Promise 状态改为 fulfilled
    • 此时,.then() 中注册的回调函数会被放入微任务队列
    • 当执行栈为空时,事件循环会先执行所有微任务,所以回调函数会被执行,打印出 ‘请求完成’ 并返回 res.data

执行顺序总结#

  1. 发起网络请求(异步任务,放入宏任务队列)
  2. 返回 Promise 对象(同步)
  3. 注册回调函数(同步)
  4. 赋值给 list 变量(同步)
  5. 打印 list(同步,输出 Promise 对象)
  6. 网络请求完成(异步任务完成)
  7. 回调函数放入微任务队列
  8. 执行栈为空,执行微任务队列中的回调函数
  9. 打印 ‘请求完成’(异步回调)

为什么不是 undefined?#

  • 因为 axios.get()立即返回一个 Promise 对象,而不是等到请求完成后才返回。
  • 所以 list 变量被赋值的是 Promise 对象,而不是 undefined。
  • 只有当你尝试直接获取异步操作的结果而不使用回调或 Promise 时,才会得到 undefined。

验证方法#

你可以在浏览器控制台或 Node.js 环境中运行这段代码,观察输出结果,会看到类似这样的顺序:

Promise {<pending>}
调用前
开始执行
调用后
请求完成 // 来自 axios.get 的回调
请求完成 // 来自 fetchData 函数的回调

结论:你的理解是完全正确的!axios.get 是异步任务,会被放入任务队列,而 console.log(list) 是同步任务,会立即执行,打印出当时的 Promise 对象。

回调函数类型#

  • 同步回调:在当前执行栈中立即执行的回调函数。

    function syncCallback(callback) {
    console.log('开始执行');
    callback(); // 立即执行回调
    console.log('执行完成');
    }
    syncCallback(() => console.log('回调执行'));
    // 输出:开始执行 → 回调执行 → 执行完成
  • 异步回调:在任务队列中等待执行的回调函数。

    function asyncCallback(callback) {
    console.log('开始执行');
    setTimeout(callback, 1000); // 1秒后执行回调
    console.log('执行完成');
    }
    asyncCallback(() => console.log('回调执行'));
    // 输出:开始执行 → 执行完成 → (1秒后)回调执行

asyncawait 详解#

基本概念#

  • async 函数
    • 用于定义一个异步函数,函数执行时会返回一个 Promise 对象。
    • 即使函数内部没有显式返回值,也会自动包装为 Promise.resolve(undefined)
  • await 关键字
    • 只能在 async 函数内部使用,用于等待一个 Promise 完成(resolve 或 reject)。
    • 当遇到 await 时,函数会暂停执行,直到 Promise 状态改变,然后继续执行并返回 Promise 的结果。

工作原理#

// 定义一个 async 函数
const getList = async () => {
// await 等待 Promise 完成
const res = await axios.get('/api/list')
return res.data
}
// 调用 async 函数
getList()
.then(data => console.log(data)) // 处理成功结果
.catch(error => console.error(error)) // 处理错误
  • 执行流程

    1. 调用 getList() 时,函数开始执行。
    2. 遇到 await axios.get('/api/list'),函数暂停执行,等待 axios 请求完成。
    3. axios 请求成功后,await 会返回响应结果,赋值给 res
    4. 函数继续执行,返回 res.data,并将其包装为 Promise.resolve(res.data)

优势#

  • 代码可读性高:避免了 Promise 链式调用(.then())的嵌套,代码结构更清晰。
  • 错误处理方便:可以使用 try/catch 捕获异步操作中的错误,与同步代码的错误处理方式一致。
  • 逻辑更直观:按照同步代码的逻辑顺序编写异步操作,符合人的思维习惯。

错误处理#

const getList = async () => {
try {
const res = await axios.get('/api/list')
return res.data
} catch (error) {
console.error('请求失败:', error)
// 可以返回默认值或重新抛出错误
return []
}
}

与 Promise 的关系#

  • async 函数是 Promise 的语法糖,内部仍使用 Promise 实现。
  • await 只能等待 Promise 对象,如果等待的不是 Promise,会自动包装为 Promise.resolve(值)
  • async 函数的返回值始终是 Promise,即使内部没有 await

实际应用场景#

  • API 调用:如示例中的 axios.get('/api/list'),等待接口返回数据。
  • 文件操作:如读取文件、写入文件等异步操作。
  • 定时器:结合 PromisesetTimeout 实现延迟操作。
  • 多步异步操作:需要按顺序执行多个异步任务时,代码更清晰。

注意事项#

  • await 只能在 async 函数内部使用,否则会报错。
  • 多个 await 会串行执行,如需并行执行多个异步操作,应使用 Promise.all()
  • async 函数内部的错误需要用 try/catch 捕获,否则会导致返回的 Promise 被拒绝。

总结async/await 是处理异步操作的现代方式,通过简化代码结构和错误处理,让异步代码更易于理解和维护,是当前前端开发中处理异步操作的主流方案。

Async/Await 的工作流程#

async function fetchData() {
console.log('开始执行');
const res = await fetch('/api/list'); // 暂停执行
console.log('请求完成');
return res.json();
}
console.log('调用前');
fetchData();
console.log('调用后');

执行顺序

  1. 打印 调用前
  2. 执行 fetchData(),打印 开始执行
  3. 遇到 await fetch('/api/list'),将 fetch 操作放入后台处理,函数暂停执行,释放主线程
  4. 主线程继续执行,打印 调用后
  5. fetch 操作完成后,将回调函数放入任务队列
  6. 事件循环检测到执行栈为空,从任务队列取出回调,恢复 fetchData() 的执行
  7. 打印 请求完成,返回结果

赞助支持

如果这篇文章对你有帮助,欢迎赞助支持!

赞助
最后更新于 2026-04-24,距今已过 130 天

部分内容可能已过时

目录