前言
Javascript非同步程式設計可以算是JS的難點之一。下面就非同步程式設計方法之一的Promise進行詳細介紹和總結。但說到Promise之前,我會簡單提一下什麼是JS非同步和回撥函式。
JS非同步
JS非同步是指在進行某些需要耗時不會立即返回結果的操作時,不會阻塞後面的操作,一旦該耗時的操作完成時,則會通知需要呼叫其結果的函式來做後續處理。這是一種非同步非阻塞的操作,也就是說任務的排列順序和執行任務是不一致的。
回撥函式
和同步操作不同,非同步操作即不會立即返回結果的操作(如發起網路請求,下載檔案,運算元據庫等)。如果我們後續的函式需要之前返回的結果,又怎樣使之前的非同步操作在其完成時通知到後續函式來執行呢?
通常,我們可以將這個函式先定義,儲存在記憶體中,將其當做引數傳入之前的非同步操作函式中,等非同步操作結束,就會呼叫執行這個函式,這個函式就叫做回撥函式(callback)。
如果不用callback,由於js會立即執行後面console.log
,導致列印出來的photo
為undefined
:
var photo = downloadPhoto('http://coolcats.com/cat.gif')
console.log(photo) //undefined
複製程式碼
使用callback時,我們將handlePhoto
當做callback傳入downloadPhoto
這個非同步函式中,那麼當圖片下載行為結束後,無論是成功還是失敗,都會執行到handlePhoto
,對photo
或者是error
進行處理。
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)
function handlePhoto (error, photo) {
if (error) console.error('Download error!', error)
else console.log('Download finished', photo)
}
console.log('Download started')
複製程式碼
那假如callback函式同樣是個非同步函式,且callback裡又嵌入了callback呢?
如此一來,巢狀太深容易引發“回撥地獄
”,即程式碼只會橫向發展,不好管理。
為此,對於那些需要連續執行的非同步操作,Promise可以是一種很好的解決辦法。
Promise
Promise的概念對於初學者來說一直很抽象,我們可以舉個例子: 比如你是個經銷商,你要去工廠訂貨,拿到貨後你才能自己銷售。那麼你和工廠之前立下一個契約,保證工廠在在完成生產後通知你,或者就算是因某種原因出錯了而無法生產也會通知到你。那麼此時這裡的契約就相當於我們要講述的promise,promise就像是個特殊的物件,連線了工廠的生產行為和你的消費行為,是生產者和消費者間的紐帶。
Promise的建立
Promise建立時,會傳給promise一個稱為excutor
執行器的函式。這個excutor
我們可以理解為生產者的生產過程函式。這個函式含有兩個引數resolve
和reject
,這倆引數也同樣是函式,用來傳遞非同步操作的結果。語法如下:
let promise = new Promise(function(resolve, reject) {
// executor
})
複製程式碼
有幾點值得說一下:
- 在promise物件建立時,
excutor
會立即執行。 - 在
resolve
和reject
是JS引擎自動建立的函式,我們無需自己建立,只需將其作為引數傳入就好。 - 建立的
promise
的內部狀態是個物件,初始時為:
{
state, //pending
result, //undefined
}
複製程式碼
一旦exucutor
執行完,要麼產生value
,要麼產生error
,此時會立即呼叫resolve
(當產生value
時)或者呼叫reject
(當產生error
)時,內部狀態也會隨之改變,如下圖所示:
注意,當excutor
裡面即使呼叫了多個resolve
和reject
,其最終還是隻執行一個,其他的都被忽略掉。
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
複製程式碼
Promise的then,catch和finally
在上面例子中,既然生產者的行為完成了,結果也傳遞出去了,那麼如何通知消費者呢?我們可以使用.then
, .catch
, 和.finally
來註冊消費者的函式,把.then
,.catch
,.finally
看做是個訂閱列表,將消費者的函式註冊於此,一旦收到結果時,就可以通知到對方進行相應的處理。
- .then的用法如下:
promise.then(
result = > resultHandle(result)
error = > errorHandle(error)
)
複製程式碼
.then()
接收2個函式,一個用來處理正常結果result
,一個用來處理error
,但一般情況下,我們也可以不用在.then()
中傳入這個error
處理的函式。
- .catch的用法如下:
promise.catch(
error = > errorHandle(error)
)
複製程式碼
其實這就是相當於promise.then(null, errorHandle)
。
.catch
會捕獲到整個非同步操作中,或者是一系列連續的非同步操作鏈中的出現的任何型別的錯誤,一旦丟擲錯誤,則會直接轉入到.catch
中進行錯誤處理。
- .finally的用法如下:
promise.finally(
finalHandle
)
複製程式碼
當promise的狀態確定時,即無論拿到的是正常結果還是錯誤資訊,總會執行這個finalHandle
函式。用.finally
可以做一些清理操作,比如發起網路請求後,可以停止loading顯示。
Promise鏈式呼叫
當有一系列的非同步操作需要一個接一個執行時,可以使用promise的呼叫鏈。
舉個例子,我們用fetch
這個方法去發起網路請求,fetch()
返回的是一個promise物件,那麼我們可以對其連續地呼叫.then
來進行一步步連續地非同步操作。注意:promise.then()
返回的是一個新的promise物件,所以我們才可以繼續對其呼叫.then
。
// 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); // (*)
}).catch(error => console.log(error));
複製程式碼
這段程式碼的作用就是先發起網路請求獲取到服務端的相應內容(其實只是響應頭),然後通過呼叫response.json()
繼續獲取response
完整的遠端資料並將其解析為JSON格式(也是非同步操作),接著根據json中user的name資訊,繼續發起網路請求,拿到使用者object及其頭像url,展示其頭像並在3秒後刪除頭像圖片。.catch
會處理上面一系列流程中出現的任何錯誤。
值得注意的是,promise.then( handleFunction )
中的handleFunction
可以返回立即值,也可以返回promise
物件。如果返回立即值,則可以直接把結果傳入到下一步的.then
進行處理,但是如果返回的是promise
物件,那麼一定要等到這個返回的promise
處理完,拿到結果後,才會進行下一步的.then
處理!可以用下圖加以理解:
總結
在JS非同步程式設計中,Promise相對於callback,具有更優的程式碼流,並且具有很好的靈活性。Promise符合自然的事物執行順序,即先做非同步操作,然後再用.then
告知下一步該做什麼。而在Callback的用法中,先得知道下一步做什麼,然後才能將其作為callback函式傳入非同步操作函式中。而且,promise在得到結果後,可以通知到多個後續的結果處理函式,.then
就像一個訂閱列表一樣。而在callback的用法中,只能傳入1個callback函式。
參考連結
Promise: javascript.info/promise-bas…
Promise MDN文件: developer.mozilla.org/en-US/docs/…
Callback Hell:callbackhell.com/