所謂的"承諾"
可能反正我寫的東西有些枯燥,但是久而久之,功力一定深厚,所以會比較小眾,堅持寫,寫到天荒地老。堅持翻譯好文。
讓我們回到介紹一章中提到的問題:回調(diào):我們有一個異步任務(wù)序列一個接一個地執(zhí)行——例如,加載腳本。我們怎樣才能把它寫好呢?
Promise 提供了一些方法來做到這一點。
在本章中,我們將討論 Promise chaining。
它是這樣的:
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;
});
其思想是通過.then處理程序鏈傳遞結(jié)果。
這里的流程是:
初始 Promise 在1秒內(nèi)解決(*),
然后 then 處理程序被調(diào)用(**)。
它返回的值被傳遞給下一個 .then 處理程序(***)
當結(jié)果沿著處理程序鏈傳遞時,我們可以看到警報調(diào)用的序列:1→2→4。

整件事都成功了,因為一個 Promise 回調(diào)。然后返回一個 Promise,這樣我們就可以在它上調(diào)用下一個.then。
當處理程序返回一個值時,它將成為該 Promise 的結(jié)果,因此下一個.then將隨之調(diào)用。
一個典型的新手錯誤:技術(shù)上,我們也可以在一個 promise 中添加多個 .then。這不是連鎖反應(yīng)。
例如:
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;
});
我們在這里所做的只是一個 Promise 的幾個處理程序。它們不會相互傳遞結(jié)果;相反,他們會獨立處理。
所有。然后在相同的 Promise 上得到相同的結(jié)果-那個 Promise 的結(jié)果。因此,在上述代碼中,所有警報顯示相同的:

在實踐中,我們很少需要對一個 Promise 使用多個處理程序。鏈接更常用。
Returning promises
在.then(handler)中使用的處理程序可以創(chuàng)建并返回 promise。
在這種情況下,進一步的處理程序會等待,直到它安定下來,然后得到它的結(jié)果。
例如:
new?Promise(function(resolve,?reject)?{
??setTimeout(()?=>?resolve(1),?1000);
}).then(function(result)?{
??alert(result);?//?1
??return?new?P
??
}).then(function(result)?{
??alert(result);?//?4
});
在這里,第一個 .then 顯示了1,并在(*)行中返回新的 Promise(…)。一秒鐘后,它進行解析,結(jié)果 (resolve的參數(shù),這里是result * 2) 被傳遞給第二個 .then 的處理器。該處理程序在(**)行中,它顯示2并執(zhí)行相同的操作。
因此輸出與前面的示例相同:1→2→4,但現(xiàn)在警報調(diào)用之間有1秒的延遲。
返回 Promise 允許我們構(gòu)建異步操作鏈。
Example: loadScript
讓我們在上一章定義的promisified loadScript中使用這個特性,按順序逐個加載腳本:
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)?{
????//?use?functions?declared?in?scripts
????//?to?show?that?they?indeed?loaded
????one();
????two();
????three();
??});
這段代碼可以用箭頭函數(shù)簡化:
loadScript("/article/promise-chaining/one.js")
??.then(script?=>?loadScript("/article/promise-chaining/two.js"))
??.then(script?=>?loadScript("/article/promise-chaining/three.js"))
??.then(script?=>?{
????//?scripts?are?loaded,?we?can?use?functions?declared?there
????one();
????two();
????three();
??});
在這里,每個loadScript調(diào)用返回一個承諾,下一個.then在解析時運行。然后,它開始加載下一個腳本。腳本一個接一個地加載。
我們可以向鏈中添加更多的異步操作。請注意,代碼仍然是“平的”-它向下增長,而不是向右。沒有“末日金字塔”的跡象。
技術(shù)上,我們可以直接在每個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?=>?{
??????//?this?function?has?access?to?variables?script1,?script2?and?script3
??????one();
??????two();
??????three();
????});
??});
});
這段代碼做了同樣的事情:依次加載3個腳本。但它“向右生長”。我們有和回調(diào)一樣的問題。
開始使用 Promise 的人有時不知道鏈接,所以他們這樣寫。一般來說,鏈接是首選。
有時直接編寫 .then 是可以的,因為嵌套函數(shù)可以訪問外部作用域。在上面的例子中,嵌套最多的回調(diào)函數(shù)可以訪問所有變量script1、script2、script3。但這是一個例外,而不是規(guī)律。
Thenables
確切地說,處理程序可能返回的不是一個promise,而是一個所謂的“thenable”對象——一個具有方法的任意對象。它會像承諾一樣被對待。
其思想是,第三方庫可以實現(xiàn)它們自己的“承諾兼容”對象。它們可以擁有一組擴展的方法,但也與本機承諾兼容,因為它們實現(xiàn)了.then。
下面是一個可執(zhí)行對象的例子:
class?Thenable?{
??constructor(num)?{
????this.num?=?num;
??}
??then(resolve,?reject)?{
????alert(resolve);?//?function()?{?native?code?}
????//?resolve?with?this.num*2?after?the?1?second
????setTimeout(()?=>?resolve(this.num?*?2),?1000);?//?(**)
??}
}
new?Promise(resolve?=>?resolve(1))
??.then(result?=>?{
????return?new?Thenable(result);?//?(*)
??})
??.then(alert);?//?shows?2?after?1000ms
JavaScript 檢查行(*)中.then處理程序返回的對象:如果它有一個名為then的可調(diào)用方法,那么它調(diào)用該方法,提供本地函數(shù)resolve,拒絕作為參數(shù)(類似于executor),并等待直到其中一個被調(diào)用。在上面的例子中,resolve(2)在1秒(**)之后被調(diào)用。然后,結(jié)果將沿著鏈向下傳遞。
這個特性允許我們將自定義對象與promise鏈集成在一起,而不必從promise繼承。
Bigger example: fetch
在前端編程中,承諾通常用于網(wǎng)絡(luò)請求。我們來看一個擴展的例子。
我們將使用fetch方法從遠程服務(wù)器加載關(guān)于用戶的信息。它有很多可選的參數(shù),在單獨的章節(jié)中介紹過,但是基本的語法非常簡單:
let?promise?=?fetch(url);
468/5000 這將向url發(fā)出一個網(wǎng)絡(luò)請求并返回一個承諾。當遠程服務(wù)器以頭響應(yīng)時,但在下載完整響應(yīng)之前,promise將使用響應(yīng)對象進行解析。
要讀取完整的響應(yīng),我們應(yīng)該調(diào)用response.text()方法:它返回一個承諾,當從遠程服務(wù)器下載全文時解析該文本,結(jié)果是該文本。
下面的代碼向用戶發(fā)出請求。json并從服務(wù)器加載它的文本:
fetch('/article/promise-chaining/user.json')
??//?.then?below?runs?when?the?remote?server?responds
??.then(function(response)?{
????//?response.text()?returns?a?new?promise?that?resolves?with?the?full?response?text
????//?when?it?loads
????return?response.text();
??})
??.then(function(text)?{
????//?...and?here's?the?content?of?the?remote?file
????alert(text);?//?{"name":?"iliakan",?"isAdmin":?true}
??});
從fetch返回的響應(yīng)對象還包括response. JSON()方法,該方法讀取遠程數(shù)據(jù)并將其解析為JSON。在我們的例子中,這更方便,所以我們切換到它。
為了簡潔起見,我們還將使用箭頭函數(shù):
//?same?as?above,?but?response.json()?parses?the?remote?content?as?JSON
fetch('/article/promise-chaining/user.json')
??.then(response?=>?response.json())
??.then(user?=>?alert(user.name));?//?iliakan,?got?user?name
現(xiàn)在讓我們對加載的用戶做一些事情。
例如,我們可以再向GitHub發(fā)出一個請求,加載用戶配置文件并顯示頭像:
//?Make?a?request?for?user.json
fetch('/article/promise-chaining/user.json')
??//?Load?it?as?json
??.then(response?=>?response.json())
??//?Make?a?request?to?GitHub
??.then(user?=>?fetch(`https://api.github.com/users/${user.name}`))
??//?Load?the?response?as?json
??.then(response?=>?response.json())
??//?Show?the?avatar?image?(githubUser.avatar_url)?for?3?seconds?(maybe?animate?it)
??.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);?//?(*)
??});
代碼的作品;請參閱有關(guān)細節(jié)的評論。然而,這里面有一個潛在的問題,對于那些開始使用 Promise 的人來說,這是一個典型的錯誤。
看看這一行(*):在角色完成展示并被移除后,我們該如何做些事情?例如,我們想要顯示一個用于編輯該用戶或其他東西的表單。就目前而言,不可能。
為了使鏈可擴展,我們需要返回一個 Promise ,它可以解決化身何時結(jié)束顯示的問題。
是這樣的:
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);
??}))
??//?triggers?after?3?seconds
??.then(githubUser?=>?alert(`Finished?showing?${githubUser.name}`));
也就是說,行(*)中的.then處理程序現(xiàn)在返回新的承諾,只有在setTimeout(**)中調(diào)用resolve(githubUser)之后才會得到解決。下一個 .then 在鏈中等待它。
作為一種良好的實踐,異步操作應(yīng)該總是返回promise。這使得我們有可能在事件發(fā)生后制定行動計劃;即使我們現(xiàn)在不打算擴展這條鏈,我們以后可能會需要它。
function?loadJson(url)?{
??return?fetch(url)
????.then(response?=>?response.json());
}
function?loadGithubUser(name)?{
??return?fetch(`https://api.github.com/users/${name}`)
????.then(response?=>?response.json());
}
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);
??});
}
//?Use?them:
loadJson('/article/promise-chaining/user.json')
??.then(user?=>?loadGithubUser(user.name))
??.then(showAvatar)
??.then(githubUser?=>?alert(`Finished?showing?${githubUser.name}`));
??//?...
Summary
如果.then(或catch/finally,沒關(guān)系)處理程序返回promise,則鏈上的其余部分將等待直到它解決。當它這樣做時,它的結(jié)果(或錯誤)將進一步傳遞。
以下是全貌:

