Javascript Promise 鏈

2023-02-17 10:53 更新

我們回顧一下 簡(jiǎn)介:回調(diào) 一章中提到的問(wèn)題:我們有一系列的異步任務(wù)要一個(gè)接一個(gè)地執(zhí)行 —— 例如,加載腳本。我們?nèi)绾螌?xiě)出更好的代碼呢?

Promise 提供了一些方案來(lái)做到這一點(diǎn)。

在本章中,我們將一起學(xué)習(xí) promise 鏈。

它看起來(lái)就像這樣:

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

它的想法是通過(guò) .then 處理程序(handler)鏈進(jìn)行傳遞 result。

運(yùn)行流程如下:

  1. 初始 promise 在 1 秒后 resolve ?(*)?,
  2. 然后 ?.then? 處理程序被調(diào)用 ?(**)?,它又創(chuàng)建了一個(gè)新的 promise(以 ?2? 作為值 resolve)。
  3. 下一個(gè) ?then? ?(***)? 得到了前一個(gè) then 的值,對(duì)該值進(jìn)行處理(*2)并將其傳遞給下一個(gè)處理程序。
  4. ……依此類(lèi)推。

隨著 result 在處理程序鏈中傳遞,我們可以看到一系列的 alert 調(diào)用:1 → 2 → 4。


這樣之所以是可行的,是因?yàn)槊總€(gè)對(duì) .then 的調(diào)用都會(huì)返回了一個(gè)新的 promise,因此我們可以在其之上調(diào)用下一個(gè) .then

當(dāng)處理程序返回一個(gè)值時(shí),它將成為該 promise 的 result,所以將使用它調(diào)用下一個(gè) .then。

新手常犯的一個(gè)經(jīng)典錯(cuò)誤:從技術(shù)上講,我們也可以將多個(gè) .then 添加到一個(gè) promise 上。但這并不是 promise 鏈(chaining)。

例如:

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

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

我們?cè)谶@里所做的只是一個(gè) promise 的幾個(gè)處理程序。它們不會(huì)相互傳遞 result;相反,它們之間彼此獨(dú)立運(yùn)行處理任務(wù)。

這是它的一張示意圖(你可以將其與上面的鏈?zhǔn)秸{(diào)用做一下比較):


在同一個(gè) promise 上的所有 .then 獲得的結(jié)果都相同 —— 該 promise 的結(jié)果。所以,在上面的代碼中,所有 alert 都顯示相同的內(nèi)容:1。

實(shí)際上我們極少遇到一個(gè) promise 需要多個(gè)處理程序的情況。使用鏈?zhǔn)秸{(diào)用的頻率更高。

返回 promise

.then(handler) 中所使用的處理程序(handler)可以創(chuàng)建并返回一個(gè) promise。

在這種情況下,其他的處理程序?qū)⒌却?settled 后再獲得其結(jié)果。

例如:

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

這里第一個(gè) .then 顯示 1 并在 (*) 行返回 new Promise(…)。1 秒后它會(huì)進(jìn)行 resolve,然后 result(resolve 的參數(shù),在這里它是 result*2)被傳遞給第二個(gè) .then 的處理程序。這個(gè)處理程序位于 (**) 行,它顯示 2,并執(zhí)行相同的行為。

所以輸出與前面的示例相同:1 → 2 → 4,但是現(xiàn)在在每次 alert 調(diào)用之間會(huì)有 1 秒鐘的延遲。

返回 promise 使我們能夠構(gòu)建異步行為鏈。

示例:loadScript

讓我們將本章所講的這個(gè)特性與在 上一章 中定義的 promise 化的 loadScript 結(jié)合使用,按順序依次加載腳本:

loadScript("/article/promise-chaining/one.js")
  .then(function(script) {
    return loadScript("/article/promise-chaining/two.js");
  })
  .then(function(script) {
    return loadScript("/article/promise-chaining/three.js");
  })
  .then(function(script) {
    // 使用在腳本中聲明的函數(shù)
    // 以證明腳本確實(shí)被加載完成了
    one();
    two();
    three();
  });

我們可以用箭頭函數(shù)來(lái)重寫(xiě)代碼,讓其變得簡(jiǎn)短一些:

loadScript("/article/promise-chaining/one.js")
  .then(script => loadScript("/article/promise-chaining/two.js"))
  .then(script => loadScript("/article/promise-chaining/three.js"))
  .then(script => {
    // 腳本加載完成,我們可以在這兒使用腳本中聲明的函數(shù)
    one();
    two();
    three();
  });

在這兒,每個(gè) loadScript 調(diào)用都返回一個(gè) promise,并且在它 resolve 時(shí)下一個(gè) .then 開(kāi)始運(yùn)行。然后,它啟動(dòng)下一個(gè)腳本的加載。所以,腳本是一個(gè)接一個(gè)地加載的。

我們可以向鏈中添加更多的異步行為。請(qǐng)注意,代碼仍然是“扁平”的 —— 它向下增長(zhǎng),而不是向右。這里沒(méi)有“厄運(yùn)金字塔”的跡象。

從技術(shù)上講,我們可以向每個(gè) loadScript 直接添加 .then,就像這樣:

loadScript("/article/promise-chaining/one.js").then(script1 => {
  loadScript("/article/promise-chaining/two.js").then(script2 => {
    loadScript("/article/promise-chaining/three.js").then(script3 => {
      // 此函數(shù)可以訪問(wèn)變量 script1,script2 和 script3
      one();
      two();
      three();
    });
  });
});

這段代碼做了相同的事兒:按順序加載 3 個(gè)腳本。但它是“向右增長(zhǎng)”的。所以會(huì)有和使用回調(diào)函數(shù)一樣的問(wèn)題。

剛開(kāi)始使用 promise 的人可能不知道 promise 鏈,所以他們就這樣寫(xiě)了。通常,鏈?zhǔn)绞鞘走x。

有時(shí)候直接寫(xiě) .then 也是可以的,因?yàn)榍短椎暮瘮?shù)可以訪問(wèn)外部作用域。在上面的例子中,嵌套在最深層的那個(gè)回調(diào)(callback)可以訪問(wèn)所有變量 script1,script2 和 script3。但這是一個(gè)例外,而不是一條規(guī)則。

Thenables

確切地說(shuō),處理程序返回的不完全是一個(gè) promise,而是返回的被稱(chēng)為 “thenable” 對(duì)象 —— 一個(gè)具有方法 .then 的任意對(duì)象。它會(huì)被當(dāng)做一個(gè) promise 來(lái)對(duì)待。

這個(gè)想法是,第三方庫(kù)可以實(shí)現(xiàn)自己的“promise 兼容(promise-compatible)”對(duì)象。它們可以具有擴(kuò)展的方法集,但也與原生的 promise 兼容,因?yàn)樗鼈儗?shí)現(xiàn)了 .then 方法。

這是一個(gè) thenable 對(duì)象的示例:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 進(jìn)行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 后顯示 2

JavaScript 檢查在 (*) 行中由 .then 處理程序返回的對(duì)象:如果它具有名為 then 的可調(diào)用方法,那么它將調(diào)用該方法并提供原生的函數(shù) resolve 和 reject 作為參數(shù)(類(lèi)似于 executor),并等待直到其中一個(gè)函數(shù)被調(diào)用。在上面的示例中,resolve(2) 在 1 秒后被調(diào)用 (**)。然后,result 會(huì)被進(jìn)一步沿著鏈向下傳遞。

這個(gè)特性允許我們將自定義的對(duì)象與 promise 鏈集成在一起,而不必繼承自 Promise。

更復(fù)雜的示例:fetch

在前端編程中,promise 通常被用于網(wǎng)絡(luò)請(qǐng)求。那么,讓我們一起來(lái)看一個(gè)相關(guān)的擴(kuò)展示例吧。

我們將使用 fetch 方法從遠(yuǎn)程服務(wù)器加載用戶信息。它有很多可選的參數(shù),我們?cè)?nbsp;單獨(dú)的一章 中對(duì)其進(jìn)行了詳細(xì)介紹,但基本語(yǔ)法很簡(jiǎn)單:

let promise = fetch(url);

執(zhí)行這條語(yǔ)句,向 url 發(fā)出網(wǎng)絡(luò)請(qǐng)求并返回一個(gè) promise。當(dāng)遠(yuǎn)程服務(wù)器返回 header(是在 全部響應(yīng)加載完成前)時(shí),該 promise 使用一個(gè) response 對(duì)象來(lái)進(jìn)行 resolve。

為了讀取完整的響應(yīng),我們應(yīng)該調(diào)用 response.text() 方法:當(dāng)全部文字內(nèi)容從遠(yuǎn)程服務(wù)器下載完成后,它會(huì)返回一個(gè) promise,該 promise 以剛剛下載完成的這個(gè)文本作為 result 進(jìn)行 resolve。

下面這段代碼向 user.json 發(fā)送請(qǐng)求,并從服務(wù)器加載該文本:

fetch('/article/promise-chaining/user.json')
  // 當(dāng)遠(yuǎn)程服務(wù)器響應(yīng)時(shí),下面的 .then 開(kāi)始執(zhí)行
  .then(function(response) {
    // 當(dāng) user.json 加載完成時(shí),response.text() 會(huì)返回一個(gè)新的 promise
    // 該 promise 以加載的 user.json 為 result 進(jìn)行 resolve
    return response.text();
  })
  .then(function(text) {
    // ……這是遠(yuǎn)程文件的內(nèi)容
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

從 fetch 返回的 response 對(duì)象還包含 response.json() 方法,該方法可以讀取遠(yuǎn)程數(shù)據(jù)并將其解析為 JSON。在我們的例子中,這更加方便,所以我們用這個(gè)方法吧。

為了簡(jiǎn)潔,我們還將使用箭頭函數(shù):

// 同上,但使用 response.json() 將遠(yuǎn)程內(nèi)容解析為 JSON
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name)); // iliakan,獲取到了用戶名

現(xiàn)在,讓我們用加載好的用戶信息搞點(diǎn)事情。

例如,我們可以再向 GitHub 發(fā)送一個(gè)請(qǐng)求,加載用戶個(gè)人資料并顯示頭像:

// 發(fā)送一個(gè)對(duì) user.json 的請(qǐng)求
fetch('/article/promise-chaining/user.json')
  // 將其加載為 JSON
  .then(response => response.json())
  // 發(fā)送一個(gè)到 GitHub 的請(qǐng)求
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // 將響應(yīng)加載為 JSON
  .then(response => response.json())
  // 顯示頭像圖片(githubUser.avatar_url)3 秒(也可以加上動(dòng)畫(huà)效果)
  .then(githubUser => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });

這段代碼可以工作,具體細(xì)節(jié)請(qǐng)看注釋。但是,這有一個(gè)潛在的問(wèn)題,一個(gè)新手使用 promise 時(shí)的典型問(wèn)題。

請(qǐng)看 (*) 行:我們?nèi)绾文茉陬^像顯示結(jié)束并被移除 之后 做點(diǎn)什么?例如,我們想顯示一個(gè)用于編輯該用戶或者其他內(nèi)容的表單。就目前而言,是做不到的。

為了使鏈可擴(kuò)展,我們需要返回一個(gè)在頭像顯示結(jié)束時(shí)進(jìn)行 resolve 的 promise。

就像這樣:

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // 3 秒后觸發(fā)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

也就是說(shuō),第 (*) 行的 .then 處理程序現(xiàn)在返回一個(gè) new Promise,只有在 setTimeout 中的 resolve(githubUser) (**) 被調(diào)用后才會(huì)變?yōu)?settled。鏈中的下一個(gè) .then 將一直等待這一時(shí)刻的到來(lái)。

作為一個(gè)好的做法,異步行為應(yīng)該始終返回一個(gè) promise。這樣就可以使得之后我們計(jì)劃后續(xù)的行為成為可能。即使我們現(xiàn)在不打算對(duì)鏈進(jìn)行擴(kuò)展,但我們之后可能會(huì)需要。

最后,我們可以將代碼拆分為可重用的函數(shù):

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

function loadGithubUser(name) {
  return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// 使用它們:
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

總結(jié)

如果 .then(或 catch/finally 都可以)處理程序返回一個(gè) promise,那么鏈的其余部分將會(huì)等待,直到它狀態(tài)變?yōu)?settled。當(dāng)它被 settled 后,其 result(或 error)將被進(jìn)一步傳遞下去。

這是一個(gè)完整的流程圖:


任務(wù)


Promise:then 對(duì)比 catch

這兩個(gè)代碼片段是否相等?換句話說(shuō),對(duì)于任何處理程序(handler),它們?cè)谌魏吻闆r下的行為都相同嗎?

promise.then(f1).catch(f2);

對(duì)比:

promise.then(f1, f2);

解決方案

簡(jiǎn)要回答就是:不,它們不相等

不同之處在于,如果 f1 中出現(xiàn) error,那么在這兒它會(huì)被 .catch 處理:

promise
  .then(f1)
  .catch(f2);

……在這兒則不會(huì):

promise
  .then(f1, f2);

這是因?yàn)?error 是沿著鏈傳遞的,而在第二段代碼中,f1 下面沒(méi)有鏈。

換句話說(shuō),.then 將 result/error 傳遞給下一個(gè) .then/.catch。所以在第一個(gè)例子中,在下面有一個(gè) catch,而在第二個(gè)例子中并沒(méi)有 catch,所以 error 未被處理。


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)