JS非同步程式設計之Promise詳解和使用總結

Lingzhi發表於2019-03-31

前言

Javascript非同步程式設計可以算是JS的難點之一。下面就非同步程式設計方法之一的Promise進行詳細介紹和總結。但說到Promise之前,我會簡單提一下什麼是JS非同步和回撥函式。

JS非同步

JS非同步是指在進行某些需要耗時不會立即返回結果的操作時,不會阻塞後面的操作,一旦該耗時的操作完成時,則會通知需要呼叫其結果的函式來做後續處理。這是一種非同步非阻塞的操作,也就是說任務的排列順序和執行任務是不一致的。

回撥函式

和同步操作不同,非同步操作即不會立即返回結果的操作(如發起網路請求,下載檔案,運算元據庫等)。如果我們後續的函式需要之前返回的結果,又怎樣使之前的非同步操作在其完成時通知到後續函式來執行呢?

通常,我們可以將這個函式先定義,儲存在記憶體中,將其當做引數傳入之前的非同步操作函式中,等非同步操作結束,就會呼叫執行這個函式,這個函式就叫做回撥函式(callback)。

如果不用callback,由於js會立即執行後面console.log,導致列印出來的photoundefined

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我們可以理解為生產者的生產過程函式。這個函式含有兩個引數resolvereject,這倆引數也同樣是函式,用來傳遞非同步操作的結果。語法如下:

let promise = new Promise(function(resolve, reject) {
  // executor 
})
複製程式碼

有幾點值得說一下:

  1. 在promise物件建立時,excutor會立即執行。
  2. resolvereject是JS引擎自動建立的函式,我們無需自己建立,只需將其作為引數傳入就好。
  3. 建立的promise的內部狀態是個物件,初始時為:
{
   state,  //pending
   result,  //undefined
}
複製程式碼

一旦exucutor執行完,要麼產生value,要麼產生error,此時會立即呼叫resolve(當產生value時)或者呼叫reject(當產生error)時,內部狀態也會隨之改變,如下圖所示:

JS非同步程式設計之Promise詳解和使用總結

注意,當excutor裡面即使呼叫了多個resolvereject,其最終還是隻執行一個,其他的都被忽略掉。

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詳解和使用總結

總結

在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/

Fetch: developer.mozilla.org/en-US/docs/…

相關文章