14、JavaScript 异步编程:Promise 的使用与反模式

JavaScript 异步编程:Promise 的使用与反模式

1. 异步函数中的错误处理

在 JavaScript 异步编程中,使用 try-catch 块处理 await 关键字等待的 Promise 非常重要。如果 try 块中等待的 Promise 被拒绝, await 表达式之后的代码将不会执行,执行流程会跳转到 catch 块。 await 关键字会抛出 Promise 的拒绝值,使 catch 块能够捕获该拒绝。

例如:

async function getUsersAndTasks() {
    try {
        const users = await fetchUsers();
        const tasks = await fetchTasks();
        // 对 users 和 tasks 进行操作
    } catch (error) {
        // 处理错误
    }
}

另外,也可以省略 try-catch 块,但调用异步函数的代码必须处理 Promise 的拒绝,可以使用 Promise 链式调用:

async function getUsersAndTasks() {
    const users = await fetchUsers();
    const tasks = await fetchTasks();
    // 对 users 和 tasks 进行操作
}

getUsersAndTasks().catch((error) => {
    /* 处理错误 */
});

或者在调用代码中使用 try-catch 块(如果使用 async-await 语法):

async function getUsersAndTasks() {
    const users = await fetchUsers();
    const tasks = await fetchTasks();
    // 对 users 和 tasks 进行操作
}

async function initApp() {
    try {
        await getUsersAndTasks();
    } catch (error) {
        // 处理错误
    }
}
2. 返回 Promise 与等待 Promise

在异步函数中忘记等待 Promise 可能会导致错误和意外输出。例如:

// 返回一个可能成功或失败的 Promise
function getPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() < 0.5) {
                resolve("success");
            } else {
                reject(new Error("failed"));
            }
        }, 1000);
    });
}

async function foo() {
    getPromise();
}

foo()
  .then(() => console.log("foo promise fulfilled"))
  .catch(() => console.log("foo promise rejected"));

上述代码中, getPromise 函数返回的 Promise 可能被拒绝,但 foo 函数返回的 Promise 总是被解决。原因是 foo 函数没有返回或等待 getPromise 函数返回的 Promise,它只是调用了 getPromise 函数,函数执行结束并隐式返回 undefined ,导致 foo 函数的 Promise 以 undefined 作为解决值被解决。

为了捕获 getPromise 函数返回的 Promise 的拒绝,可以采取以下方法:
- 返回 getPromise 函数返回的 Promise

async function foo() {
    return getPromise();
}

foo()
  .then(() => console.log("foo promise fulfilled"))
  .catch(() => console.log("foo promise failed"));

返回 Promise 后, foo 函数的 Promise 会与 getPromise 函数返回的 Promise 有相同的结果。
- 等待 getPromise 函数返回的 Promise

async function foo() {
    await getPromise();
}

foo()
  .then(() => console.log("foo promise fulfilled"))
  .catch(() => console.log("foo promise failed"));

如果等待的 Promise 被拒绝,其拒绝值会在异步函数内部抛出。由于 foo 函数内部没有 try-catch 块,Promise 的拒绝会导致 foo 函数的 Promise 也以相同的拒绝原因被拒绝。需要注意的是,如果 getPromise 函数返回的 Promise 被解决, foo 函数的 Promise 会以 undefined 作为解决值,因为没有从 foo 函数中显式返回任何值。
- 等待 getPromise 函数返回的 Promise 并使用 try-catch 块包围

async function foo() {
    try {
        await getPromise();
    } catch (error) {
        console.log("inside catch block of foo function");
        return "error caught in foo";
    }
}

foo()
  .then(() => console.log("foo promise fulfilled"))
  .catch(() => console.log("foo promise failed"));

等待 getPromise 调用会捕获 Promise 的拒绝,使 catch 块执行。但 foo 函数返回的 Promise 总是被解决,因为 catch 块没有抛出错误或返回被拒绝的 Promise。如果只在 catch 块中抛出错误,最好省略 try-catch 块,让 Promise 的拒绝自动拒绝 foo 函数的 Promise。此外,如果 Promise 被解决且 catch 块未执行,没有显式返回任何值, foo 函数的 Promise 会以 undefined 作为解决值。在 await 表达式前添加 return 关键字可以解决这个问题。

3. 等待非 Promise 值

await 关键字通常用于等待 Promise 解决,但也可以用于非 Promise 值。例如:

const printRandomNumber = async () => {
    const randomNum = await Math.floor(Math.random() * 10);
    console.log(randomNum);
};

printRandomNumber();
console.log("before printing random number");

执行上述代码时,会发现最后一行的 console.log 语句在随机数打印之前被记录,尽管函数调用在最后一个 console.log 语句之前。这是因为当 await 关键字与非 Promise 值一起使用时,会创建一个新的 Promise,并以该值作为解决值。 await 表达式之后的代码会像在解决处理程序中一样异步执行,只有在同步代码执行结束后才会执行异步代码。

4. 微任务和任务

执行 DOM 事件监听器、 setTimeout setInterval 回调需要调度“任务”,任务会被放入任务队列,直到事件循环处理它们。而 Promise 的解决或拒绝处理程序的执行需要调度“微任务”。微任务(ECMAScript 规范称为作业)的优先级高于“任务”,会在以下情况后处理:
- 每个回调之后(前提是调用栈为空)。
- 每个任务之后。

“任务”按其在任务队列中的入队顺序执行,事件循环每一轮只执行一个任务。而微任务会一直处理,直到微任务队列为空。如果一个微任务调度了另一个微任务,该微任务也会被处理,这可能导致事件循环陷入无限循环。

例如:

console.log("start");

setTimeout(() => {
    console.log("setTimeout callback with 500ms delay");
}, 500);

Promise.resolve()
  .then(() => {
        console.log("first 'then' callback");
    })
  .then(() => {
        console.log("second 'then' callback");
    })
  .then(() => {
        console.log("third 'then' callback");
    });

setTimeout(() => {
    console.log("setTimeout callback with 0ms delay");
}, 0);

console.log("end");

执行上述代码的步骤如下:
1. 创建一个任务来执行脚本,开始同步代码的执行。
2. 执行第一个 console.log 语句,在控制台记录 “start”。
3. 调用 setTimeout 函数,延迟 500ms,会在后台启动一个定时器,定时器到期后会将执行 setTimeout 回调的任务放入任务队列。
4. 调用 Promise.resolve 创建一个已解决的 Promise,为执行其解决处理程序,将一个微任务放入微任务队列。
5. 调用 setTimeout 函数,延迟 0ms,调度一个任务来执行其回调。
6. 同步执行到达最后一个 console.log 语句,记录 “end”。此时调用栈为空,事件循环可以开始处理调度的任务和微任务。
7. 由于微任务在每个任务和每个回调之后(调用栈为空时)处理,同步代码执行结束后,事件循环会处理微任务队列中的微任务,记录 “first ‘then’ callback”。
8. 第一个 then 方法的回调函数隐式返回 undefined ,导致该 then 方法返回的 Promise 以 undefined 作为解决值,将另一个微任务放入微任务队列以执行该 Promise 的解决处理程序。
9. 微任务会一直处理,直到微任务队列为空,记录 “second ‘then’ callback”。
10. 第二个 then 方法的回调函数隐式返回 undefined ,将另一个微任务放入微任务队列以执行该 Promise 的解决处理程序。
11. 记录 “third ‘then’ callback”。
12. 忽略第三个 then 方法返回的 Promise 的解决,所有微任务处理完毕,微任务队列为空,事件循环开始处理任务队列中的第一个任务。
13. 任务队列中的第一个任务是第二个 setTimeout 调用的任务,处理该任务会记录 “setTimeout callback with 0ms delay”。
14. 最后处理第一个 setTimeout 调用的任务,记录 “setTimeout callback with 500ms delay”。

5. Promise 相关的反模式

在 JavaScript 异步编程中,有一些常见的 Promise 相关反模式需要避免:
- 不必要地使用 Promise 构造函数
许多 JavaScript 开发者,尤其是对 Promise 经验不足的开发者,会不必要地使用 Promise 构造函数创建 Promise。例如:

function fetchData(url) {
    return new Promise((resolve, reject) => {
        fetch(url)
          .then((res) => res.json(res))
          .then(resolve)
          .catch(reject);
    });
}

上述代码可以正常工作,但使用 Promise 构造函数是不必要的,因为 fetch 函数已经返回一个 Promise。可以重写为:

function fetchData(url) {
    return fetch(url).then((res) => res.json(res));
}

重写后的 fetchData 函数更简洁、易读,避免了创建不必要的 Promise,并且调用该函数的代码可以捕获和处理任何错误。不必要地使用 Promise 构造函数还可能导致问题,如果忘记在 Promise 链中添加 catch 方法,HTTP 请求期间抛出的任何错误都不会被捕获。
- 错误处理不当
编写使用 Promise 的代码时,重要的规则是要么捕获并处理错误,要么返回 Promise 让调用代码捕获和处理。例如:

function fetchData(url) {
    fetch(url).then((response) => response.json());
}

fetchData("https://jsonplaceholder.typicode.com/todos/1")
  .then((data) => console.log(data))
  .catch((error) => console.log(error));

上述代码会抛出错误,因为 fetchData 函数没有返回 Promise,也不允许调用代码进行任何错误处理。可以通过在 fetch(...) 前添加 return 关键字来修复:

function fetchData(url) {
    return fetch(url).then((response) => response.json());
}

这样调用代码就可以负责使用响应数据和处理任何错误。

总结

在 JavaScript 异步编程中,正确处理 Promise 及其错误是非常重要的。避免不必要地使用 Promise 构造函数,确保正确处理错误,合理使用 await 关键字和微任务、任务的调度,能够编写出更健壮、高效的异步代码。

JavaScript 异步编程:Promise 的使用与反模式

6. 反模式之将 Promise 拒绝转换为解决

有时候开发者会不小心将 Promise 的拒绝转换为解决,这会隐藏潜在的错误,使调试变得困难。例如:

function getData() {
    return new Promise((resolve, reject) => {
        // 模拟一个可能失败的异步操作
        if (Math.random() < 0.5) {
            reject(new Error('Operation failed'));
        } else {
            resolve('Success');
        }
    })
  .catch(() => {
        // 错误处理中直接 resolve,将拒绝转换为解决
        resolve('Fallback value');
    });
}

getData()
  .then((data) => console.log(data))
  .catch((error) => console.log(error));

在上述代码中, getData 函数中的 catch 块捕获到错误后,直接调用 resolve 将 Promise 转换为解决状态。这会导致调用者无法得知操作实际上失败了。正确的做法是在 catch 块中进行必要的处理后,重新抛出错误或者返回一个被拒绝的 Promise,让调用者能够正确处理错误。

7. 反模式之异步执行器函数

使用异步函数作为 Promise 构造函数的执行器函数也是一种反模式。例如:

function asyncExecutor() {
    return new Promise(async (resolve, reject) => {
        try {
            const result = await someAsyncOperation();
            resolve(result);
        } catch (error) {
            reject(error);
        }
    });
}

这里使用了异步函数作为执行器,虽然代码可以正常工作,但这是不必要的。 Promise 构造函数的执行器是同步执行的,使用异步函数会增加不必要的复杂性。可以直接返回异步操作的 Promise,而不需要使用 Promise 构造函数来包装。例如:

function asyncExecutor() {
    return someAsyncOperation();
}
8. 总结与最佳实践

为了避免上述提到的反模式,在编写 JavaScript 异步代码时,可以遵循以下最佳实践:
| 反模式 | 避免方法 |
| ---- | ---- |
| 不必要地使用 Promise 构造函数 | 优先使用已有的异步函数返回的 Promise,避免手动包装 |
| 错误处理不当 | 确保每个 Promise 链都有合适的错误处理,要么在当前函数中处理,要么返回 Promise 让调用者处理 |
| 将 Promise 拒绝转换为解决 | 在 catch 块中进行必要处理后,重新抛出错误或返回被拒绝的 Promise |
| 异步执行器函数 | 直接返回异步操作的 Promise,不使用 Promise 构造函数包装 |

此外,对于异步函数中的错误处理,要合理使用 try-catch 块和 await 关键字。在使用 await 时,要确保正确处理可能的拒绝情况。对于微任务和任务的调度,要理解它们的执行顺序和优先级,避免因微任务调度不当导致事件循环陷入无限循环。

9. 流程图示例

下面是一个简单的流程图,展示了异步函数中错误处理的流程:

graph TD;
    A[开始异步函数] --> B{Promise 是否被拒绝};
    B -- 是 --> C[跳转到 catch 块];
    B -- 否 --> D[继续执行 await 之后的代码];
    C --> E[处理错误];
    D --> F[完成异步函数];
    E --> F;

这个流程图清晰地展示了在异步函数中,如果 await 的 Promise 被拒绝,执行流程会跳转到 catch 块进行错误处理;如果 Promise 被解决,则继续执行 await 之后的代码。

通过遵循这些最佳实践和理解异步编程的原理,可以编写出更健壮、高效且易于维护的 JavaScript 异步代码。

考虑柔性负荷的综合能源系统低碳经济优化调度【考虑碳交易机制】(Matlab代码实现)内容概要:本文围绕“考虑柔性负荷的综合能源系统低碳经济优化调度”展开,重点研究在碳交易机制下如何实现综合能源系统的低碳化经济性协同优化。通过构建包含风电、光伏、储能、柔性负荷等多种能源形式的系统模型,结合碳交易成本能源调度成本,提出优化调度策略,以降低碳排放并提升系统运行经济性。文中采用Matlab进行仿真代码实现,验证了所提模型在平衡能源供需、平抑可再生能源波动、引导柔性负荷参调度等方面的有效性,为低碳能源系统的设计运行提供了技术支撑。; 适合人群:具备一定电力系统、能源系统背景,熟悉Matlab编程,从事能源优化、低碳调度、综合能源系统等相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①研究碳交易机制对综合能源系统调度决策的影响;②实现柔性负荷在削峰填谷、促进可再生能源消纳中的作用;③掌握基于Matlab的能源系统建模优化求解方法;④为实际综合能源项目提供低碳经济调度方案参考。; 阅读建议:建议读者结合Matlab代码深入理解模型构建求解过程,重点关注目标函数设计、约束条件设置及碳交易成本的量化方式,可进一步扩展至多能互补、需求响应等场景进行二次开发仿真验证。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值