学习目标:
可以看懂原有的用promise实现的代码,也可以做到根据日常需求判断什么场景下使用promise,以及promise怎么去用。
了解promise链式调用,并可以应用到日常需求中。
学习内容:
- promise链式调用规则。
- promise链式调用出现的意义。
- 回调地狱及回调地狱解决。
- 多个异步函数同步调用实现示例。
学习产出:
promise链式调用规则
上篇讲到,promise对象中三个方法:.then()、.catch().、finally,它们都会返回promise对象。
因此可以实现链式调用。
且调用规则为:
- then:一般当前promise变为已兑现状态时才会走then,且即使 .then() 缺少返回 Promise 对象的回调函数,处理程序仍会继续到链的下一个链式调用。因此,在最终的 .catch() 之前,可以安全地省略每个链式调用中处理已拒绝状态的回调函数。
- catch:一般当前promise抛出异常或者调用reject,且没有被前面的then函数处理时(有的异常需要阻断链条就直接在then(null,handleRejected)里处理了)调用。 catch(failureCallback) 等同于 then(null, failureCallback)。
- finally:一般当前promise变为已兑现状态时会走finally。
有了这层规则,再看多个异步函数同步执行的实现:
-
假设每个异步函数都改为返回Promise对象形式,且当前异步函数内容执行完后才会修改当前promise的状态。
- -
开始时调用异步函数1,异步函数触发异步调用逻辑、但不会等异步调用完就会返回promise对象。
-
promise对象作为代理承担了异步函数的执行状态管理,即异步函数执行完才会返回成功或者失败,进而走下面的流程。
-
对上面返回的promise对象调用then方法,在这个then里调用第二个要执行的异步函数。
-
then方法是在上面返回的promise兑现时才去执行的,且返回了一个新的promise。
-
第二个异步函数又返回的新的promise对象, 再去调用then方法,then方法里调用第三个异步函数…。
-
上面的then()里,只是(定义)调用了异步函数调用成功的情况,失败的情况,最后放到catch里面进行处理。
可见promise只是规定了这三个异步函数的调用顺序,对每个异步函数添加了promise代理,在未来特定阶段去控制这些异步函数的触发。由于每个函数都会返回新的promise,从而最终实现了同步的链式调用。
promise链式调用出现的意义:
异步函数实现同步调用 ,之前的方式和使用promise两种对比,简单举例:
之前的方式:
/* createAudioFileAsync定义 */
function createAudioFileAsync(audioSettings, successCallback, failureCallback) {
const audioData = generateAudioData(audioSettings); // 模拟生成音频数据的函数
// writeFile异步调用
fs.writeFile(audioSettings.path, audioData, (err) => {
if (err) {
return failureCallback(err);
}
successCallback(`文件已创建:${audioSettings.path}`);
});
};
// 成功的回调函数
function successCallback(result) {
console.log("音频文件创建成功:" + result);
}
// 失败的回调函数
function failureCallback(error) {
console.log("音频文件创建失败:" + error);
}
调用:
createAudioFileAsync(audioSettings, successCallback, failureCallback);
promise形式:
如果设置 createAudioFileAsync() 的返回值为 Promise 对象,则可以把回调函数附加到它的then上面:
function createAudioFileAsync(audioSettings) {
return new Promise((resolve, reject) => {
const buffer = createAudioBuffer(audioSettings); // 生成音频缓冲区
// 异步处理音频数据(如编码为 MP3 并保存)
encodeAudioToMp3(buffer, audioSettings, (encodedData) => {
saveFile(encodedData, settings.path, (success) => {
if (success) {
resolve(`音频文件创建成功:${settings.path}`);
} else {
reject(new Error("文件保存失败"));
}
});
}, (encodeErr) => {
reject(encodeErr);
});
});
}
调用:
createAudioFileAsync(audioSettings).then(successCallback,failureCallback);
通过对比可发现,使用Promise使得调用链更加扁平化更加清晰。
在这之前如果需要实现连续执行两个或以上异步方法,上一个异步方法执行完,再去执行下一个,且需要带上上一个的执行结果去执行下一个时,用之前的回调方式写则会出现“回调地狱”。
回调地狱及回调地狱解决
回调地狱:
借用mdn网站上的例子,简单讲述下,看不懂也没关系,本来就是很绕的东西,也可以直接跳过看Promise解决的部分。
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`得到最终结果:${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
说明:
有三个异步方法doSomething、doSomethingElse、doThirdThing。
这三个方法相互依赖调用,调用当前方法前依赖上一个的调用结果,于是就出现上述代码。
调用步骤:
(1)先执行异步方法doSomething里的逻辑,比如call后端api获取结果等;
(2)获取到(1)的结果后,执行doSomethingElse函数,这个函数里拿到(1)的结果进行自己的逻辑处理,处理完后返回处理后的结果。
(3)doThirdThing获取到(2)中的结果后,处理doThirdThing里自己的逻辑,包括执行匿名函数里的console.log。
(4)failureCallback贯穿所有异步调用,即无论在哪一步调用异常都能执行到failureCallback去处理异常信息。
这里只简单表述,感兴趣对可访问回调地狱详细链接:http://callbackhell.com/。
回调地狱解决(使用Promise) :
doSomething()
.then((result)=> doSomethingElse(result)) // 拿到doSomething返回结果 只有一行代码箭头函数会默认加return 即隐式返回值
.then((newResult)=> doThirdThing(newResult)) // 拿到doSomethingElse返回结果
.then((finalResult)=>{
console.log(`得到最终结果:${finalResult}`);
})
.catch(failureCallback)
即doSomething、doSomethingElse、doThirdThing都返回Promise对象,先调用第一个doSomething返回promise,那么then方法里会首先等待这个 promise 的敲定,敲定后then会接收两个回调函数,这里then只处理调用了resolve(已兑现状态)的,拿到resolve中传递的值,再去调用下一个异步函数。
而reject调用后的场景统一在catch中处理(注意:回调函数里会接收到它的兑现值,而不是 Promise 本身)。
对比之前的实现逻辑来看,变成单链条调用是不是简单许多。
当然通过 async/await也可以实现上述情况,且也是扁平易理解,但此处主要说Promise,因此先掠过。
多个异步函数同步调用实现示例
deepSeek获取模型列表 ,并使用其中一个模型进行对话补全。
(此处知只是简单的对话补全,主要是为了巩固promise的使用)。
查看deepseek官方文档发现,可以直接使用openai的sdk去调用dp的api。
dsRobot.js
import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://api.deepseek.com/v1',
apiKey: 'sk-533b5d7ecc6d482dadbb476f6e21bc6f',
dangerouslyAllowBrowser: true,
});
/* 其中openai.models.list和 openai.chat.completions.create 返回的均是Promise对象 感兴趣可以去扒下源码 */
/* 获取模型列表 官网提供的例子 */
export async function modelList() {
const models = await openai.models.list();
for await (const model of models) {
// 仅仅输出用来查看返回格式
console.log('=========', model);
}
// 这里添加返回 因为后面需要用到
return models.data;
}
/* 对话补全 官网提供的例子 */
export async function chatCompletions() {
const completion = await openai.chat.completions.create({
messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
model: 'deepseek-chat',
});
console.log(completion.choices[0].message.content);
// 这里添加返回
return completion.choices[0].message.content;
}
/* 获取模型列表 直接返回promise形式 */
export function modelListForPromise() {
return openai.models.list().then(models => models.data);
}
/* 对话补全 直接使用promise形式 */
export function chatCompletionsForPromise(param) {
return openai.chat.completions.create(param)
.then(completion => completion.choices[0].message.content);
}
可见chatCompletionsForPromise的执行依赖于modelListForPromise的结果,因此调用顺序为:
<template>
<a-button type="primary" @click="clickCall">
点击触发
</a-button>
</template>
<script setup>
function clickCall() {
modelListForPromise()
.then(models => {
if (models.length === 0) {
console.error('没有可用的模型');
return Promise.reject(new Error('没有可用的模型'));
}
return { model: models[0].id, messages: [{ role: 'user', content: '你好,世界!' }] };
})
.then((param) => {
console.log('参数:', param);
return chatCompletionsForPromise(param);
})
.then((response) => {
console.log('聊天响应:', response);
})
.catch((error) => {
console.error('发生错误:', error);
});
}
// 也附加上async/await调用方式
async function clickCall1() {
try {
const models = await modelList();
console.log('models:', models);
if (models.length === 0) {
console.error('没有可用的模型');
return;
}
const response = await chatCompletions(models[0].id);
console.log('聊天响应:', response);
}
catch (error) {
console.error('发生错误:', error);
}
}
</script>
await 会等待promise兑现后,拿到兑现值。