Javascript Promise

2023-02-17 10:53 更新

想象一下,你是一位頂尖歌手,粉絲沒(méi)日沒(méi)夜地詢(xún)問(wèn)你下首歌什么時(shí)候發(fā)。

為了從中解放,你承諾(promise)會(huì)在單曲發(fā)布的第一時(shí)間發(fā)給他們。你給了粉絲們一個(gè)列表。他們可以在上面填寫(xiě)他們的電子郵件地址,以便當(dāng)歌曲發(fā)布后,讓所有訂閱了的人能夠立即收到。即便遇到不測(cè),例如錄音室發(fā)生了火災(zāi),以致你無(wú)法發(fā)布新歌,他們也能及時(shí)收到相關(guān)通知。

每個(gè)人都很開(kāi)心:你不會(huì)被任何人催促,粉絲們也不用擔(dān)心錯(cuò)過(guò)歌曲發(fā)行。

這是我們?cè)诰幊讨薪?jīng)常遇到的事兒與真實(shí)生活的類(lèi)比:

  1. “生產(chǎn)者代碼(producing code)”會(huì)做一些事兒,并且會(huì)需要一些時(shí)間。例如,通過(guò)網(wǎng)絡(luò)加載數(shù)據(jù)的代碼。它就像一位“歌手”。
  2. “消費(fèi)者代碼(consuming code)”想要在“生產(chǎn)者代碼”完成工作的第一時(shí)間就能獲得其工作成果。許多函數(shù)可能都需要這個(gè)結(jié)果。這些就是“粉絲”。
  3. Promise 是將“生產(chǎn)者代碼”和“消費(fèi)者代碼”連接在一起的一個(gè)特殊的 JavaScript 對(duì)象。用我們的類(lèi)比來(lái)說(shuō):這就是就像是“訂閱列表”?!吧a(chǎn)者代碼”花費(fèi)它所需的任意長(zhǎng)度時(shí)間來(lái)產(chǎn)出所承諾的結(jié)果,而 “promise” 將在它(譯注:指的是“生產(chǎn)者代碼”,也就是下文所說(shuō)的 executor)準(zhǔn)備好時(shí),將結(jié)果向所有訂閱了的代碼開(kāi)放。

這種類(lèi)比并不十分準(zhǔn)確,因?yàn)?JavaScipt 的 promise 比簡(jiǎn)單的訂閱列表更加復(fù)雜:它們還擁有其他的功能和局限性。但以此開(kāi)始挺好的。

Promise 對(duì)象的構(gòu)造器(constructor)語(yǔ)法如下:

let promise = new Promise(function(resolve, reject) {
  // executor(生產(chǎn)者代碼,“歌手”)
});

傳遞給 new Promise 的函數(shù)被稱(chēng)為 executor。當(dāng) new Promise 被創(chuàng)建,executor 會(huì)自動(dòng)運(yùn)行。它包含最終應(yīng)產(chǎn)出結(jié)果的生產(chǎn)者代碼。按照上面的類(lèi)比:executor 就是“歌手”。

它的參數(shù) resolve 和 reject 是由 JavaScript 自身提供的回調(diào)。我們的代碼僅在 executor 的內(nèi)部。

當(dāng) executor 獲得了結(jié)果,無(wú)論是早還是晚都沒(méi)關(guān)系,它應(yīng)該調(diào)用以下回調(diào)之一:

  • ?resolve(value)? —— 如果任務(wù)成功完成并帶有結(jié)果 ?value?。
  • ?reject(error)? —— 如果出現(xiàn)了 error,?error? 即為 error 對(duì)象。

所以總結(jié)一下就是:executor 會(huì)自動(dòng)運(yùn)行并嘗試執(zhí)行一項(xiàng)工作。嘗試結(jié)束后,如果成功則調(diào)用 resolve,如果出現(xiàn) error 則調(diào)用 reject。

由 new Promise 構(gòu)造器返回的 promise 對(duì)象具有以下內(nèi)部屬性:

  • ?state? —— 最初是 ?"pending"?,然后在 ?resolve? 被調(diào)用時(shí)變?yōu)?nbsp;?"fulfilled"?,或者在 ?reject? 被調(diào)用時(shí)變?yōu)?nbsp;?"rejected"?。
  • ?result? —— 最初是 ?undefined?,然后在 ?resolve(value)? 被調(diào)用時(shí)變?yōu)?nbsp;?value?,或者在 ?reject(error)? 被調(diào)用時(shí)變?yōu)?nbsp;?error?。

所以,executor 最終將 promise 移至以下?tīng)顟B(tài)之一:


稍后我們將看到“粉絲”如何訂閱這些更改。

下面是一個(gè) promise 構(gòu)造器和一個(gè)簡(jiǎn)單的 executor 函數(shù),該 executor 函數(shù)具有包含時(shí)間(即 setTimeout)的“生產(chǎn)者代碼”:

let promise = new Promise(function(resolve, reject) {
  // 當(dāng) promise 被構(gòu)造完成時(shí),自動(dòng)執(zhí)行此函數(shù)

  // 1 秒后發(fā)出工作已經(jīng)被完成的信號(hào),并帶有結(jié)果 "done"
  setTimeout(() => resolve("done"), 1000);
});

通過(guò)運(yùn)行上面的代碼,我們可以看到兩件事兒:

  1. executor 被自動(dòng)且立即調(diào)用(通過(guò) ?new Promise?)。
  2. executor 接受兩個(gè)參數(shù):?resolve? 和 ?reject?。這些函數(shù)由 JavaScript 引擎預(yù)先定義,因此我們不需要?jiǎng)?chuàng)建它們。我們只需要在準(zhǔn)備好(譯注:指的是 executor 準(zhǔn)備好)時(shí)調(diào)用其中之一即可。
  3. 經(jīng)過(guò) 1 秒的“處理”后,executor 調(diào)用 resolve("done") 來(lái)產(chǎn)生結(jié)果。這將改變 promise 對(duì)象的狀態(tài):


這是一個(gè)成功完成任務(wù)的例子,一個(gè)“成功實(shí)現(xiàn)了的諾言”。

下面則是一個(gè) executor 以 error 拒絕 promise 的示例:

let promise = new Promise(function(resolve, reject) {
  // 1 秒后發(fā)出工作已經(jīng)被完成的信號(hào),并帶有 error
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

對(duì) reject(...) 的調(diào)用將 promise 對(duì)象的狀態(tài)移至 "rejected"


總而言之,executor 應(yīng)該執(zhí)行一項(xiàng)工作(通常是需要花費(fèi)一些時(shí)間的事兒),然后調(diào)用 resolve 或 reject 來(lái)改變對(duì)應(yīng)的 promise 對(duì)象的狀態(tài)。

與最初的 “pending” promise 相反,一個(gè) resolved 或 rejected 的 promise 都會(huì)被稱(chēng)為 “settled”。

這只能有一個(gè)結(jié)果或一個(gè) error

executor 只能調(diào)用一個(gè) resolve 或一個(gè) reject。任何狀態(tài)的更改都是最終的。

所有其他的再對(duì) resolve 和 reject 的調(diào)用都會(huì)被忽略:

let promise = new Promise(function(resolve, reject) {
  resolve("done");

  reject(new Error("…")); // 被忽略
  setTimeout(() => resolve("…")); // 被忽略
});

這的宗旨是,一個(gè)被 executor 完成的工作只能有一個(gè)結(jié)果或一個(gè) error。

并且,resolve/reject 只需要一個(gè)參數(shù)(或不包含任何參數(shù)),并且將忽略額外的參數(shù)。

以 ?Error? 對(duì)象 reject

如果什么東西出了問(wèn)題,executor 應(yīng)該調(diào)用 reject。這可以使用任何類(lèi)型的參數(shù)來(lái)完成(就像 resolve 一樣)。但建議使用 Error 對(duì)象(或繼承自 Error 的對(duì)象)。這樣做的理由很快就會(huì)顯而易見(jiàn)。

resolve/reject 可以立即進(jìn)行

實(shí)際上,executor 通常是異步執(zhí)行某些操作,并在一段時(shí)間后調(diào)用 resolve/reject,但這不是必須的。我們還可以立即調(diào)用 resolve 或 reject,就像這樣:

let promise = new Promise(function(resolve, reject) {
  // 不花時(shí)間去做這項(xiàng)工作
  resolve(123); // 立即給出結(jié)果:123
});

例如,當(dāng)我們開(kāi)始做一個(gè)任務(wù)時(shí),但隨后看到一切都已經(jīng)完成并已被緩存時(shí),可能就會(huì)發(fā)生這種情況。

這挺好。我們立即就有了一個(gè) resolved 的 promise。

?state? 和 ?result? 都是內(nèi)部的

Promise 對(duì)象的 state 和 result 屬性都是內(nèi)部的。我們無(wú)法直接訪問(wèn)它們。但我們可以對(duì)它們使用 .then/.catch/.finally 方法。我們?cè)谙旅鎸?duì)這些方法進(jìn)行了描述。

消費(fèi)者:then,catch

Promise 對(duì)象充當(dāng)?shù)氖?executor(“生產(chǎn)者代碼”或“歌手”)和消費(fèi)函數(shù)(“粉絲”)之間的連接,后者將接收結(jié)果或 error??梢酝ㄟ^(guò)使用 .then 和 .catch 方法注冊(cè)消費(fèi)函數(shù)。

then

最重要最基礎(chǔ)的一個(gè)就是 ?.then?。

語(yǔ)法如下:

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

.then 的第一個(gè)參數(shù)是一個(gè)函數(shù),該函數(shù)將在 promise resolved 且接收到結(jié)果后執(zhí)行。

.then 的第二個(gè)參數(shù)也是一個(gè)函數(shù),該函數(shù)將在 promise rejected 且接收到 error 信息后執(zhí)行。

例如,以下是對(duì)成功 resolved 的 promise 做出的反應(yīng):

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

// resolve 運(yùn)行 .then 中的第一個(gè)函數(shù)
promise.then(
  result => alert(result), // 1 秒后顯示 "done!"
  error => alert(error) // 不運(yùn)行
);

第一個(gè)函數(shù)被運(yùn)行了。

在 reject 的情況下,運(yùn)行第二個(gè):

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

// reject 運(yùn)行 .then 中的第二個(gè)函數(shù)
promise.then(
  result => alert(result), // 不運(yùn)行
  error => alert(error) // 1 秒后顯示 "Error: Whoops!"
);

如果我們只對(duì)成功完成的情況感興趣,那么我們可以只為 .then 提供一個(gè)函數(shù)參數(shù):

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

promise.then(alert); // 1 秒后顯示 "done!"

catch

如果我們只對(duì) error 感興趣,那么我們可以使用 null 作為第一個(gè)參數(shù):.then(null, errorHandlingFunction)?;蛘呶覀円部梢允褂?nbsp;.catch(errorHandlingFunction),其實(shí)是一樣的:

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

// .catch(f) 與 promise.then(null, f) 一樣
promise.catch(alert); // 1 秒后顯示 "Error: Whoops!"

.catch(f) 調(diào)用是 .then(null, f) 的完全的模擬,它只是一個(gè)簡(jiǎn)寫(xiě)形式。

清理:finally

就像常規(guī) try {...} catch {...} 中的 finally 子句一樣,promise 中也有 finally

調(diào)用 .finally(f) 類(lèi)似于 .then(f, f),因?yàn)楫?dāng) promise settled 時(shí) f 就會(huì)執(zhí)行:無(wú)論 promise 被 resolve 還是 reject。

finally 的功能是設(shè)置一個(gè)處理程序在前面的操作完成后,執(zhí)行清理/終結(jié)。

例如,停止加載指示器,關(guān)閉不再需要的連接等。

把它想象成派對(duì)的終結(jié)者。無(wú)論派對(duì)是好是壞,有多少朋友參加,我們都需要(或者至少應(yīng)該)在它之后進(jìn)行清理。

代碼可能看起來(lái)像這樣:

new Promise((resolve, reject) => {
  /* 做一些需要時(shí)間的事,之后調(diào)用可能會(huì) resolve 也可能會(huì) reject */
})
  // 在 promise 為 settled 時(shí)運(yùn)行,無(wú)論成功與否
  .finally(() => stop loading indicator)
  // 所以,加載指示器(loading indicator)始終會(huì)在我們繼續(xù)之前停止
  .then(result => show result, err => show error)

請(qǐng)注意,finally(f) 并不完全是 then(f,f) 的別名。

它們之間有重要的區(qū)別:

  1. ?finally? 處理程序(handler)沒(méi)有參數(shù)。在 ?finally? 中,我們不知道 promise 是否成功。沒(méi)關(guān)系,因?yàn)槲覀兊娜蝿?wù)通常是執(zhí)行“常規(guī)”的完成程序(finalizing procedures)。
  2. 請(qǐng)看上面的例子:如你所見(jiàn),?finally? 處理程序沒(méi)有參數(shù),promise 的結(jié)果由下一個(gè)處理程序處理。

  3. ?finally? 處理程序?qū)⒔Y(jié)果或 error “傳遞”給下一個(gè)合適的處理程序。
  4. 例如,在這結(jié)果被從 finally 傳遞給了 then

    new Promise((resolve, reject) => {
      setTimeout(() => resolve("value"), 2000)
    })
      .finally(() => alert("Promise ready")) // 先觸發(fā)
      .then(result => alert(result)); // <-- .then 顯示 "value"

    正如我們所看到的,第一個(gè) promise 返回的 value 通過(guò) finally 被傳遞給了下一個(gè) then。

    這非常方便,因?yàn)?nbsp;finally 并不意味著處理一個(gè) promise 的結(jié)果。如前所述,無(wú)論結(jié)果是什么,它都是進(jìn)行常規(guī)清理的地方。

    下面是一個(gè) promise 返回結(jié)果為 error 的示例,讓我們看看它是如何通過(guò) finally 被傳遞給 catch 的:

    new Promise((resolve, reject) => {
      throw new Error("error");
    })
      .finally(() => alert("Promise ready")) // 先觸發(fā)
      .catch(err => alert(err));  // <-- .catch 顯示這個(gè) error
  5. ?finally? 處理程序也不應(yīng)該返回任何內(nèi)容。如果它返回了,返回的值會(huì)默認(rèn)被忽略。
  6. 此規(guī)則的唯一例外是當(dāng) ?finally? 處理程序拋出 error 時(shí)。此時(shí)這個(gè) error(而不是任何之前的結(jié)果)會(huì)被轉(zhuǎn)到下一個(gè)處理程序。

總結(jié):

  • ?finally? 處理程序沒(méi)有得到前一個(gè)處理程序的結(jié)果(它沒(méi)有參數(shù))。而這個(gè)結(jié)果被傳遞給了下一個(gè)合適的處理程序。
  • 如果 ?finally? 處理程序返回了一些內(nèi)容,那么這些內(nèi)容會(huì)被忽略。
  • 當(dāng) ?finally? 拋出 error 時(shí),執(zhí)行將轉(zhuǎn)到最近的 error 的處理程序。

如果我們正確使用 ?finally?(將其用于常規(guī)清理),那么這些功能將很有用。

我們可以對(duì) settled 的 promise 附加處理程序

如果 promise 為 pending 狀態(tài),.then/catch/finally 處理程序(handler)將等待它的結(jié)果。

有時(shí)候,當(dāng)我們向一個(gè) promise 添加處理程序時(shí),它可能已經(jīng) settled 了。

在這種情況下,這些處理程序會(huì)立即執(zhí)行:

// 下面這 promise 在被創(chuàng)建后立即變?yōu)?resolved 狀態(tài)
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done!(現(xiàn)在顯示)

請(qǐng)注意這使得 promise 比現(xiàn)實(shí)生活中的“訂閱列表”方案強(qiáng)大得多。如果歌手已經(jīng)發(fā)布了他們的單曲,然后某個(gè)人在訂閱列表上進(jìn)行了注冊(cè),則他們很可能不會(huì)收到該單曲。實(shí)際生活中的訂閱必須在活動(dòng)開(kāi)始之前進(jìn)行。

Promise 則更加靈活。我們可以隨時(shí)添加處理程序(handler):如果結(jié)果已經(jīng)在了,它們就會(huì)執(zhí)行。

示例:loadScript

接下來(lái),讓我們看一下關(guān)于 promise 如何幫助我們編寫(xiě)異步代碼的更多實(shí)際示例。

我們從上一章獲得了用于加載腳本的 loadScript 函數(shù)。

這是基于回調(diào)函數(shù)的變體,記住它:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

讓我們用 promise 重寫(xiě)它。

新函數(shù) loadScript 將不需要回調(diào)。取而代之的是,它將創(chuàng)建并返回一個(gè)在加載完成時(shí) resolve 的 promise 對(duì)象。外部代碼可以使用 .then 向其添加處理程序(訂閱函數(shù)):

function loadScript(src) {
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Script load error for ${src}`));

    document.head.append(script);
  });
}

用法:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

我們立刻就能發(fā)現(xiàn) promise 相較于基于回調(diào)的模式的一些好處:

promise callback
promise 允許我們按照自然順序進(jìn)行編碼。首先,我們運(yùn)行 loadScript 和 .then 來(lái)處理結(jié)果。 在調(diào)用 loadScript(script, callback) 時(shí),我們必須有一個(gè) callback 函數(shù)可供使用。換句話(huà)說(shuō),在調(diào)用 loadScript 之前,我們必須知道如何處理結(jié)果。
我們可以根據(jù)需要,在 promise 上多次調(diào)用 .then。每次調(diào)用,我們都會(huì)在“訂閱列表”中添加一個(gè)新的“粉絲”,一個(gè)新的訂閱函數(shù)。在下一章將對(duì)此內(nèi)容進(jìn)行詳細(xì)介紹:Promise 鏈 只能有一個(gè)回調(diào)。

因此,promise 為我們提供了更好的代碼流和靈活性。但其實(shí)還有更多相關(guān)內(nèi)容。我們將在下一章看到。

任務(wù)


用 promise 重新解決?

下列這段代碼會(huì)輸出什么?

let promise = new Promise(function(resolve, reject) {
  resolve(1);

  setTimeout(() => resolve(2), 1000);
});

promise.then(alert);

解決方案

輸出為:1。

第二個(gè)對(duì) resolve 的調(diào)用會(huì)被忽略,因?yàn)橹挥械谝淮螌?duì) reject/resolve 的調(diào)用才會(huì)被處理。進(jìn)一步的調(diào)用都會(huì)被忽略。


基于 promise 的延時(shí)

內(nèi)建函數(shù) ?setTimeout? 使用了回調(diào)函數(shù)。請(qǐng)創(chuàng)建一個(gè)基于 promise 的替代方案。

函數(shù) delay(ms) 應(yīng)該返回一個(gè) promise。這個(gè) promise 應(yīng)該在 ms 毫秒后被 resolve,所以我們可以向其中添加 .then,像這樣:

function delay(ms) {
  // 你的代碼
}

delay(3000).then(() => alert('runs after 3 seconds'));

解決方案

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

delay(3000).then(() => alert('runs after 3 seconds'));

請(qǐng)注意,在此任務(wù)中 resolve 是不帶參數(shù)調(diào)用的。我們不從 delay 中返回任何值,只是確保延遲即可。


帶有 promise 的圓形動(dòng)畫(huà)

重寫(xiě)任務(wù) 帶回調(diào)的圓圈動(dòng)畫(huà) 的解決方案中的 ?showCircle? 函數(shù),以使其返回一個(gè) promise,而不接受回調(diào)。

新的用法:

showCircle(150, 150, 100).then(div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

以任務(wù) 帶回調(diào)的圓圈動(dòng)畫(huà) 的解決方案為基礎(chǔ)。


解決方案

使用沙箱打開(kāi)解決方案。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)