JavaScript中的Promise是如何工作的

513 阅读7分钟

Promise

想象你是一位顶级歌手,你的粉丝夜以继日的询问你即将发布的新歌。

为了得到一些缓解,你许下承诺,当歌曲发布时会第一时间邮件通知他们。你给了你的粉丝们一份名单,他们可以在清单中填写自己的电子邮件地址,当新歌发布时,所有填写了电子邮件的粉丝都会收到通知。即使出现严重问题,比如录音室着火,导致你无法发布歌曲,他们仍然会收到通知。

每个人都很开心:你是因为不会再承受拥挤的人群,而粉丝是因为他们不再会错过这首歌。

我们在编程中经常会遇到与之类似的情况:

  1. 一个“生成代码”,可以做某事并需要时间。 例如,一些通过网络加载数据的代码。 这就是“歌手”。
  2. “消费代码”一旦准备好就需要“生成代码”的结果。 许多函数可能需要该结果。 这些人就是“粉丝”。
  3. Promise就是将“生成代码”和“消费代码”联系在一起的特殊的JavaScript对象。打个比方:这就是“订阅列表”。 “生成代码”需要花费任何时间来生成承诺的结果,并且“承诺”在准备好时将该结果提供给所有订阅的代码。

这个类比并不是非常准确,因为 JavaScript Promise 比简单的订阅列表更复杂:它们有额外的功能和限制。

Promise 对象的构造函数语法是:

let promise = new Promise(function(resolve,reject){
    // executor (the producing code, "singer")
})

传递给 new Promise 的函数称为执行器。 当新的 Promise 创建时,执行器会自动运行。 它包含最终应产生结果的生成代码。 按照上面的比喻:执行者就是“歌手”。

它的参数resolve和reject是JavaScript本身提供的回调。 我们的代码仅位于执行器内部。

当执行器获得结果时,无论是早还是晚,都没有关系,它应该调用以下回调之一:

  • resolve(value)——如果执行器的工作完成的很成功,value为传入完成工作得到的结果。
  • reject(error)——如果发生了错误,error为错误对象。

总结一下:执行器自动运行并尝试完成工作。 当工作完成时,如果成功则调用resolve,如果出现错误则调用reject

new Promise 构造函数返回的 Promise 对象具有以下内部属性:

  • state——初始化为"pending",要么在调用resolve时变为fulfilled,要么在调用reject时变为"rejected"
  • result——初始化为undefined,在调用resolve(value)时改变为value或者调用reject(error)时改变为error

所以执行器最终将promise改变为其中一个状态:

屏幕截图 2024-05-04 220903.png

随后我们将会看到“粉丝”如何订阅这些改变。

下面是一个 Promise 构造函数和一个简单的执行器函数的示例,其中“生成代码”需要消耗时间(通过 setTimeout):

let promise = new Promise(function(reslove,reject){
    //这个函数会自动执行
    
    //一秒后工作将会完成,并且结果是“done”
    setTimeout(()=>resolve("done"),1000);
})

通过运行上面的代码我们可以看到两件事:

  1. 执行器被立即自动调用(通过new Promise)。

  2. 执行器接收两个参数:resolve 和reject。 这些函数是由 JavaScript 引擎预先定义的,因此我们不需要创建它们。 我们应该只在准备好时才调用其中之一。 经过一秒的“处理”后,执行器调用resolve(“done”)来产生结果。 这会改变 Promise 对象的状态:

屏幕截图 2024-05-04 222017.png

这是一个成功完成工作的例子,一个“fulfilled promise”。

现在是执行器因错误而拒绝承诺的示例:

let promise = new Promise(function(resolve,reject){
    setTimeout(()=>reject(new Error("Whoops")),1000)
})

调用reject(...)将promise对象转换为"rejected"状态

屏幕截图 2024-05-04 222652.png

总而言之,执行器应该执行一项工作(通常需要时间),然后调用resolve或reject来更改相应promise对象的状态。

已解决或被拒绝的承诺称为“已解决”,而不是最初的“pending”承诺。

提醒:一旦promise对象的状态发生改变就不能再改变

执行器应该只调用一个resolve或一个reject。 任何状态更改都是不可变的。

所有后续的resolve和reject的调用都会被忽略:

let promise = new Promise(function(resolve,reject){
    reslove("done");
    
    reject(new Error("..."));//忽略
    setTimeout(()=>resolve("..."));//忽略
})

消费代码:then、catch

Promise 对象充当执行器(“生成代码”或“歌手”)和消费函数(“粉丝”)之间的链接,它将接收结果或错误。 可以使用 .then 和 .catch 方法注册(订阅)消费函数。

then

最重要、最根本的是.then。

语法是:

promise.then(
    function(result){/*promise为fulfilled时执行的代码*/},
    function(error){/*promise为rejected时执行的代码*/}
);

.then 的第一个参数是一个函数,该函数在 Promise 得到解决并接收结果时运行。

.then 的第二个参数是一个函数,当 Promise 被拒绝并收到错误时运行。

例如,以下是对成功解决的承诺的反应:

let promise = new Promise(function(resolve,reject){
    setTimeout(()=>resolve("done"),1000);
});
promise.then(
    result => alert(result),//一秒后显示done
    error => alert(error)//不会执行
);

第一个函数已执行。

如果被拒绝,则第二个:

let promise = new Promise(function(resolve,reject){
    setTimeout(()=>reject(new Error("Whoops!")),1000);
});

promise.then(
    result => alert(result),//不会执行
        error => alert(error)//一秒后显示"Whoops!"
)

如果我们只对成功完成感兴趣,那么我们可以只向 .then 提供一个函数参数:

let promise = new Promise(resolve =>{
   setTimeout(()=>resolve("done"),1000);
});

promise.then(alert);//一秒后显示“done”

catch

如果我们只对错误感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。 或者我们可以使用 .catch(errorHandlingFunction),它是完全相同的:

let promise = new Promise((resolve,reject) => {
    setTimeout(()=>reject(new Error(Whoops!)),1000);
})
//.catch(f)和.then(null,f)的作用是相同的
promise.catch(alert);

调用 .catch(f) 完全类似于 .then(null, f),它只是一个简写。

Cleanup: finally

就像常规 try {...} catch {...} 中有一个finally 子句一样,promise 中也有finally 子句。

调用 .finally(f) 与 .then(f, f) 类似,当 Promise 解决时,无论是解决还是拒绝,f 总是运行。

finally 的想法是设置一个处理程序,用于在前面的操作完成后执行清理/终结。

例如, 关闭不再需要的连接等。

将其视为派对终结者。 无论聚会好坏,无论有多少朋友参加,我们仍然需要(或至少应该)在聚会结束后进行清理。

代码可能如下所示:

new Promise((resolve,reject) => {
    //做某些花费时间的任务
})
    .finally(() => close connection )
    .then(result => show result,err => show err)

请注意,finally(f) 并不完全是 then(f,f) 的别名。

有一些重要的区别:

  1. finally 处理程序没有参数。 最后我们不知道这个承诺是否成功。 没关系,因为我们的任务通常是执行“一般”完成程序。请看一下上面的示例:如您所见,finally 处理程序没有参数,并且 Promise 结果由下一个处理程序处理。
  2. 最终处理程序将结果或错误“传递”到下一个合适的处理程序。例如,这里结果通过finally传递给then:
new Promise((resolve,reject) => {
    setTimeout(()=>resolve("value"),2000);
})
    .finally(()=>alert("Promise ready"))
    .then(result => alert(result));

正如您所看到的,第一个 Promise 返回的值通过 finally 传递到下一个 then。

这非常方便,因为 finally 并不意味着处理承诺结果。 如前所述,这是一个进行一般清理的地方,无论结果如何。

这是一个错误的例子,让我们看看它是如何通过finally来捕获的:

new Promise((resolve,reject) => {
    throw new Error("error");
})
    .finally(()=>alert(“Promise ready”))
    .catch(err => alert(err))
  1. finally 处理程序也不应该返回任何内容。 如果是这样,返回的值将被默默地忽略。此规则的唯一例外是当finally 处理程序抛出错误时。 然后,此错误将转到下一个处理程序,而不是任何先前的结果。

总结一下:

  • finally 处理程序不会获得前一个处理程序的结果(它没有参数)。 该结果将被传递到下一个合适的处理程序。
  • 如果finally处理程序返回一些东西,它就会被忽略。
  • 当finally抛出错误时,执行将转到最近的错误处理程序。