0

0

深入理解 JavaScript await:解决异步函数执行顺序不一致问题

花韻仙語

花韻仙語

发布时间:2025-11-29 15:06:00

|

559人浏览过

|

来源于php中文网

原创

深入理解 JavaScript await:解决异步函数执行顺序不一致问题

本文深入探讨了javascript中`await`关键字的正确使用方式,重点解决在异步函数调用中因未正确标记函数为`async`或未返回promise而导致的执行顺序不一致问题。通过详细解释其工作原理和提供修正后的代码示例,文章旨在帮助开发者确保异步操作按预期顺序执行,避免常见的并发陷阱,从而提升代码的可预测性和稳定性。

理解 await 关键字与异步执行流程

在JavaScript中,async/await 语法糖极大地简化了异步代码的编写,使其看起来更像同步代码。然而,不正确的使用方式可能导致出乎意料的执行顺序。一个常见的误解是,只要在一个函数调用前加上 await,就能够确保该函数内部的所有异步操作完成后再继续执行后续代码。实际上,await 的行为与它所等待的表达式类型密切相关。

考虑以下代码片段:

console.log("1");
await reloadScenarios();
console.log("2");

const reloadScenarios = () => {
    if (token) {
        getScenario()
            .then(({ scenarios }) => {
                console.log("3");
                const transformedScenarios = scenarios.map(option => ({
                    scenario: option.name,
                    description: option.categories.name,
                    value: option._id
                }));
                setOptions(transformedScenarios);
            })
            .catch((error) => {
                console.error('Failed to fetch scenario options:', error);
            });
    }
};

当执行这段代码时,预期的输出顺序可能是 1, 3, 2。然而,实际的输出却是 1, 2, 3。这种不一致的执行顺序表明 await reloadScenarios() 并没有等待 reloadScenarios 函数内部的异步操作完成。

为什么 await 没有生效?

await 关键字的核心作用是暂停一个 async 函数的执行,直到它所等待的 Promise 解决(fulfilled)或拒绝(rejected)。如果 await 后面的表达式不是一个 Promise,JavaScript 会将其立即解析为一个已解决的 Promise。

立即学习Java免费学习笔记(深入)”;

在上述示例中,reloadScenarios 函数存在两个关键问题:

  1. 它不是一个 async 函数: await 只能在 async 函数内部使用。虽然我们在全局上下文(或另一个 async 函数)中 await 了 reloadScenarios,但 reloadScenarios 本身是一个普通的同步函数。
  2. 它没有返回一个 Promise: 即使 reloadScenarios 内部调用了 getScenario() 这个可能返回 Promise 的函数,但 reloadScenarios 函数体本身并没有将这个 Promise 返回。因此,当 reloadScenarios() 被调用时,它会立即执行,并返回 undefined(因为没有显式返回任何值)。

由于 reloadScenarios() 返回 undefined,await 关键字会立即将 undefined 解析为一个已解决的 Promise,并允许程序继续执行 console.log("2")。而 getScenario().then(...) 中的异步回调(打印 3)则会在后台继续执行,并在稍后完成。

BlackBox AI
BlackBox AI

AI编程助手,智能对话问答助手

下载

解决方案:标记函数为 async 并返回 Promise

要实现预期的 1, 3, 2 执行顺序,需要对 reloadScenarios 函数进行两处关键修改:

1. 将函数标记为 async

使用 async 关键字修饰函数,使其成为一个异步函数。一个 async 函数总是隐式地返回一个 Promise。

const reloadScenarios = async () => {
    // ... 函数体 ...
};

当一个函数被标记为 async 后,await reloadScenarios() 就会等待这个由 async 函数隐式返回的 Promise。

2. 确保 async 函数返回其内部的 Promise

即使函数被标记为 async,如果它内部的异步操作(如 getScenario() 返回的 Promise)没有被 return 语句传递出去,那么 async 函数可能会在内部 Promise 解决之前就返回一个已解决的 Promise。为了让外部的 await 真正等待到内部的异步操作完成,我们需要显式地返回内部的 Promise。

const reloadScenarios = async () => {
    if (token) {
        // 确保返回 getScenario() 链式调用的 Promise
        return getScenario()
            .then(({ scenarios }) => {
                console.log("3");
                const transformedScenarios = scenarios.map(option => ({
                    scenario: option.name,
                    description: option.categories.name,
                    value: option._id
                }));
                setOptions(transformedScenarios);
            })
            .catch((error) => {
                console.error('Failed to fetch scenario options:', error);
                // 捕获错误后,可以重新抛出错误,或者返回一个拒绝的Promise
                // 或者返回一个已解决的Promise,取决于错误处理策略
                throw error; // 重新抛出错误,以便外部 await 也能捕获
            });
    }
    // 如果 token 不存在,也应该返回一个 Promise,例如一个已解决的 Promise
    return Promise.resolve();
};

通过这两处修改,reloadScenarios 现在是一个真正的异步函数,并且它返回的 Promise 会在其内部的 getScenario() 及其 then / catch 链完成时才会被解决或拒绝。这样,外部的 await reloadScenarios() 就能正确地暂停执行,直到 console.log("3") 完成,然后才会继续执行 console.log("2"),从而得到预期的 1, 3, 2 顺序。

修正后的完整代码示例

// 假设 token 和 getScenario(), setOptions() 已定义
let token = 'some-token'; // 示例 token
const getScenario = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ scenarios: [{ name: 'Scenario A', categories: { name: 'Category X' }, _id: 'id1' }] });
        }, 500); // 模拟异步请求
    });
};
const setOptions = (options) => {
    console.log("Set options:", options);
};

const reloadScenarios = async () => {
    if (token) {
        try {
            // 显式返回 Promise,确保外部 await 等待
            const { scenarios } = await getScenario(); // 也可以在这里直接 await
            console.log("3");

            const transformedScenarios = scenarios.map(option => ({
                scenario: option.name,
                description: option.categories.name,
                value: option._id
            }));
            setOptions(transformedScenarios);
            return transformedScenarios; // 返回处理后的数据
        } catch (error) {
            console.error('Failed to fetch scenario options:', error);
            throw error; // 重新抛出错误,让外部 await 的 try...catch 捕获
        }
    }
    // 如果 token 不存在,返回一个已解决的 Promise,避免 await 挂起
    return Promise.resolve([]);
};

// 在一个 async 函数中调用 reloadScenarios
(async () => {
    console.log("1");
    try {
        await reloadScenarios();
        console.log("2");
    } catch (error) {
        console.error("Error during reloadScenarios:", error);
    }
})();

关键点与最佳实践

  • await 必须在 async 函数中使用: 这是一个语法规定。
  • async 函数总是返回一个 Promise: 即使你没有显式地 return Promise.resolve() 或 Promise.reject(),async 函数也会将其返回值包装在一个 Promise 中。
  • 确保 async 函数返回其内部的关键 Promise: 如果 async 函数内部有多个异步操作,并且你希望外部的 await 等待特定的异步操作完成,请确保该操作返回的 Promise 最终被 return 语句传递出去。
  • 错误处理: 在 async/await 中,可以使用 try...catch 块来处理 Promise 被拒绝的情况,这比 .catch() 链更具可读性。
  • 默认返回值: 当异步操作不满足条件时(例如本例中 token 不存在),确保 async 函数依然返回一个 Promise(例如 Promise.resolve() 或 Promise.reject()),以避免 await 接收到 undefined。

总结

正确理解和使用 async/await 对于编写可维护、可预测的异步 JavaScript 代码至关重要。核心在于:await 关键字仅在 async 函数内部有效,并且它会等待一个 Promise 的解决。要确保异步操作按预期顺序执行,必须将相关函数标记为 async,并确保这些 async 函数显式地返回它们所依赖的内部异步操作的 Promise。遵循这些原则,可以有效避免因执行顺序混乱而导致的程序行为异常。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

554

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

731

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

656

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

551

2023.09.20

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

8

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.7万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.2万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号