本文開始
在這一篇文章有提到,以前的 JS 要利用 callback function 來安排非同步功能的執行先後順序的時候,很容易就會形成 callback hell 的問題,而 Promise 的 chaining 功能就可以很好的解決這個問題。
所以,本文內文即為記錄此篇教學文提及有關 Promise chaining 的部分。
這邊就來個 Promise chaining 的範例
1 | new Promise((resolve, reject) => { |
來解析一下,上面範例,透過 then
形成的 Promise chaining 的執行過程
- 最一開始的 Promise 會呼叫 resolve 並將結果 1 往後面的 stream 傳遞
- 接著,step 2 的
.then
啟動,當它回傳值的時候,會自動創出一個 Promise 物件,並夾帶回傳的內容,以上面範例來講的話, step 2 的.then
會創出一個 Promise 物件,並夾帶 result * 2 的值。 - 接著,step 3 的
.then
啟動,並自動創出一個 Promise 物件,夾帶著回傳的內容,傳到後面的 stream 。 - … and so on
所以,上面的範例在各 .then
的值的傳遞過程為1 → 2 → 4
。
⭐ 我們可以這樣以 .then
做出一個 Promise chaining 的效果,就是因為 .then
會自動回傳一個 Promise 物件,如此一來,我們才能在後面繼續用其他 .then
串接起來。
Returning Promises
雖然,上面的範例展示了 then 可以自動回傳 promise 物件,但是,這樣自動回傳的 Promise 物件我們就沒辦法做一些額外的操作,比如希望延遲幾秒後才啟動後面的 .then
。
那這個時候,我們就可以利用傳入 .then
的 function,來製作要回傳的 Promise 物件,進而在這個要回傳的 Promise 物件的 executor 中,做一些我們想要的操作。
直接上範例
1 | new Promise((resolve, reject) => { |
這個範例執行的步驟流程跟之前範例的流程完全一樣,只不過我們透過傳入 .then
的 function 回傳新的 Promise 物件,並在其內部的 executor 裡面定義 setTimeout
function,來操縱後面的 .then
會被延遲啟動,因為, .then
一定要等前方的 Promise 的狀態為 settled 時,才會被啟動。
這個範例的結果傳遞過程一樣為 1 → 2 → 4
。
而這樣的效果,就可以讓我們做出一個由非同步動作串聯起來的 Promise chaining。
Example: loadScript
在教學文章有提及到了 loadScript 的範例,並利用 Promise chaining 來撰寫它
1 | function loadScript(src) { |
可以發現用這樣的 .then 的 chaining 寫法,程式碼就不會往右邊成長,而是往下串接,這樣子也就解決了 callback hell 的問題囉。
.callback hell 臭味的 promise chaining 寫法
而上面的 .then
部分也可以用下面這種寫法
1 | loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js") |
這樣的寫法,是不是有點 callback hell 的臭味出現😅,
但是,這樣的寫法,有一個好處,就是內部巢狀的 .then 的 handler function 可以拿到外圍的 .then 的變數內容,
什麼意思呢?
以上面的範例為例,
nested function 1 可以拿到 outer function 的 script 內容,
nested function 2 可以拿到 outer function 的 script 內容 和 nested function 1 的 script 內容。
就取決於你的情境你需要怎麼使用。
❗ Thenables
準確一點來說,在傳入 then 的 handler function 它回傳的內容不一定要是 Promise 物件,
而是叫做 thenable
的物件,也就是該物件自身也擁有一個叫做 .then
的成員函式,如此一來它也可以像 Promise 物件一樣,使用 .then
方法,並做出 chaining 的效果。
這個 thenable
物件的想法,可能會被用在某些第三方套件,它們會自製屬於他們自己的 promise-compatible(可以跟 promise 一起使用的) 物件。
如此一來,第三方套件可以在這些自製的 thenable
物件中,定義屬於它們自己需要使用的方法,還可以跟原生 JS 的 promise 合在一起使用。
以下就是 thenable
物件的製作範例
1 | class Thenable { |
上面範例執行的過程
.在 step 1 回傳自製的 Thenable 物件,並使用它的 then 成員方法
.接著,在 Thenable 物件的 then 方法中,傳入 result => alert(result)
所以,在 Thenable 物件的 alert(resolve)
部分,才會顯示 function() { native code }
。
.執行在 setTimeoout(() =>resolve(this.num *2), 1000)
,並在 1 秒後呼叫 resolve(this.num * 2)
,可以看的出來我們將 this.num *2 當作參數傳入 resolve function 裡面,
接著,會傳入到 result => alert(result)
的 alert(result) 裡面,這也就是為什麼一秒後會顯示 2 的原因。
Bigger example: fetch
教學文章又用了一個時常在專案中會使用到的取得 server 資料的方法 fetch
作為範例。fetch
最基本的用法,就是下方這個寫法
1 | const promise = fetch(url) |
上面這個內容,就是去 url 這個地方去取得資料,並在完成取得資料後,會回傳一個 Promise 物件。
在 fetch 回傳的 promise 物件,我們會利用 response.text()
這個方法,來解析出回傳回來的內容,如果直接用 console.log 取看 response 會發現它是 [object Response] 的內容。
直接先上範例
1 | fetch('https://javascript.info/article/promise-chaining/user.json') |
但是, response.text()
回傳的會是 string 型別,我們無法以物件的方式去取得它裡面的屬性內容。
所以,我們會使用另外一個 response.json()
將回傳的內容轉成 json 格式,如此一來就可調用回傳物件的屬性值囉。
1 | fetch('https://javascript.info/article/promise-chaining/user.json') |
接下來,改變一下上面的範例
1 | fetch('https://randomuser.me/api/') |
先講一下,這邊的範例,試圖要做到的效果是,
圖片出現三秒之後,跳出彈窗,顯示使用者個名稱,並且在按下彈窗的 ok 鍵後,把圖片刪掉。
但是!!!
上面的範例,不會是我們想要的執行效果,step 1 執行完之後,會立馬執行 step 2 的 .then
內容,而這個 step 2 的 userName 會是 undefined。
接著,圖片 load 完成出後,等三秒被刪掉,但是,resolve 的內容就不會被執行。
以上的問題反映了,我們需要把 then 執行的時機點也延遲三秒。
那我們就要回憶一下 then 執行的時機點會是在前一個 Promise 物件 settled 之後,才會執行,那代表我們只要把前面的 Promise 物件延遲 3 秒後才改成 settled 的狀態,如此一來就能達到我們想要的效果。接著,上面的範例,如果,我們希望在顯示完 user 的圖片之後,再跳個提示窗的話,我們就要再加入一些下面的內容
所以,我們要改一下上面的範例
1 | fetch('https://randomuser.me/api/') |
經過上面的改動就可以變成我們想要的效果。
透過回傳一個新的 Promise 物件,如此一來,後面 chaining 的 then 就會等這個新的 Promise 物件 settled 之後,才執行 alert 的功能。
最後,把上面的範例,分成好幾個 function,來讓這個一連串的 Promise chaining 動作,更有可讀性
1 | function loadJson(url) { |
以上就改完囉~
Conclusion
- 可以利用 promise chaining 來解決 callback hell 的問題。
- 可以達成 promise chaining 的功能是因為
.then
會回傳 promise 物件,如此一來,才能在後面接.then
以達到 chaining 的功能。
以下為依據 .then
回傳的結果來執行相對應的 handler 的圖