作者 | Brilliant Open Web團隊
編輯 | Brilliant Open Web團隊
前面在完結了響應式布局系列介紹之後,接下來我們把目光放到現代前端開發當中的一個重要課題:異步程式設計。在早期 AJAX 項目實踐過程中,我們逐漸發現基于異步回調的機制無法很好地管理多個異步狀态,一不留神就墜入回調地獄當中。是以 ES6 引入的 Promise 對象,它所帶來的鍊式調用寫法線性地描述了多個異步函數的順序執行過程,才會讓我們感到耳目一新。Promise 涵蓋了很大一塊内容,是以本篇隻将 Promise 鍊式調用這比較有意思的部分摘出來,細細品味這其中的過程。
Promise 的基本用法
在分析 Promise 的鍊式調用之前,我們先簡要介紹下 Promise 的基本用法。一般在使用 Promise 對象的時候,首先需要對其進行執行個體化:
let promise = new Promise((resolve, reject) => { if (/* 操作成功 */) { resolve(value) } else { reject(error) }})
這時執行個體化的 promise 對象維護着一個狀态,resolve() 和 reject() 則是用于觸發 promise 變更的開關。
我們可以把任意一個異步操作過程包裹進 Promise 的回調函數體裡面,當異步操作成功或失敗時分别調用 resolve(value) 或 reject(error) 變更 Promise 對象的狀态,這樣就成功地把任意形式的異步操作狀态轉換為格式統一的 Promise 狀态。
Promise 提供了 .then(onFulfilled, onRejected) 和 .catch(onRejected) 方法用于注冊監聽 Promise 對象的狀态變更。下面的表格給出了觸發函數、狀态變更以及執行回調之間的對應關系:
觸發函數 | Promise 狀态變更 | 執行回調 |
resolve(value) | fullfilled | onFulfilled(value) |
reject(error) | rejected | onRejected(error) |
下面舉個例子來示範 Promise 對象的基本使用方法:
let promise = new Promise((resolve) => { // 利用 setTimeout 1 秒後觸發 promise 的狀态變更 // 同時傳回字元串 'hello world!' setTimeout(() => { resolve('hello world!') }, 1000)})// 1 秒後執行 onFulfilled// 列印異步傳回結果 'hello world!'promise.then( value => { console.log(value) }, // 在本示例中 onRejected 不會觸發 error => { console.error(error) })
上面這段示例代碼示範了單個 Promise 對象的建立、狀态觸發、事件監聽與觸發的流程,要實作鍊式調用還需要通過一種機制,将多個 Promise 對象串聯起來。
Promise 鍊式調用
Promise 的鍊式調用是通過 .then() 和 .catch() 方法實作的,其中 .catch() 等價于 .then(null, onRejected),是以我們在接下來的内容當中隻需研究 .then() 方法的性質即可。
.then() 方法除了用于注冊監聽函數之外,本身也會建立并傳回一個 Promise 對象,這個 Promise 對象用于表征回調函數的執行情況。下面結合例子來示範這一過程:
// 建立初始 Promise 對象并成功傳回 undefinedlet p0 = new Promise(resolve => resolve())// 執行 onFulfilled 并列印 '[step 1]'let p1 = p0.then(() => { console.log('[step 1]')})// 列印 trueconsole.log(p1 instanceof Promise)// 執行 onFulfilled 并列印 '[step 2]'let p2 = p1.then(() => { console.log('[step 2]')})// 列印 trueconsole.log(p2 instanceof Promise)
可以看到調用 .then() 方法所傳回的 p1 和 p2 都是 Promise 對象。接着我們去掉所有中間變量來簡化代碼:
// 建立初始 Promise 對象并成功傳回 undefinednew Promise(resolve => resolve()) // 執行 onFulfilled 并列印 '[step 1]' .then(() => { console.log('[step 1]') }) // 執行 onFulfilled 并列印 '[step 2]' .then(value => { console.log('[step 2]') })
這樣 Promise 的鍊式調用便出現了,代碼一下子也變得清晰了起來。接下來我們進一步分析這個鍊式調用過程中的狀态傳遞情況。
Promise 狀态傳遞
前面提到 .then() 方法傳回的 Promise 對象的作用是表征回調函數的執行情況,當回調函數執行成功時 Promise 狀态将變更為 'fulfilled',執行過程抛出異常 Promise 狀态則變成 'rejected',是以上一節例子當中 p0 與 p1,p1 與 p2 是沒有直接聯系的。下面的例子将示範鍊式調用過程中出錯時狀态變更與傳遞的情況:
// 步驟 0,傳回 p0(fulfilled)new Promise(resolve => resolve()) // 步驟 1,傳回 p1(fulfilled) .then(() => { console.log('成功') }) // 步驟 2,傳回 p2(rejected) .then(() => { throw Error('抛錯') }) // 步驟 3,傳回 p3(fulfilled) .catch(() => { console.log('捕獲錯誤') }) // 步驟 4,傳回 p4(fulfilled) .then(() => { console.log('結束') })
在鍊式調用過程當中,假如某個環節的 Promise 不存在相應狀态的監聽回調函數,那麼這個 Promise 的狀态将會往下透傳:
// 步驟 0,傳回 p0(fulfilled)new Promise(resolve => resolve()) // 步驟 1,傳回 p1 // 無 onFulfilled 回調,透傳 p0 狀态 .catch(() => { console.log('捕獲[1]') }) // 步驟 2,傳回 p2 // 無 onFulfilled 回調,透傳 p0 狀态 .catch(() => { console.log('捕獲[2]') }) // 步驟 3,傳回 p3(fulfilled) .then(() => { console.log('成功[1]') }) // 步驟 4,傳回 p4(rejected) .then(() => { throw Error('抛錯') }) // 步驟 5,傳回 p5 // 無 onRejected 回調,透傳 p4 狀态 .then(() => { console.log('成功[2]') }) // 步驟 6,傳回 p6(fulfilled) .catch(() => { console.log('捕獲[3]') })
在這個過程當中,步驟 1、2、5 由于沒有相關狀态的回調函數,是以将上一個 Promise 的狀态持續透傳了下去。這一特點的最常見的應用場景是,我們可以将一個鍊式調用過程中所産生的錯誤統一放到最後處理:
run1() .then(run2) .then(run3) .then(run4) .catch(error => { console.log(error) })
Promise 值傳遞
.then() 方法會将回調函數的執行結果記錄下來,并作為下一個 onFulfilled 回調的參數将其傳遞下去:
new Promise(resolve => resolve()) .then(() => { return '[p1]' }) // 列印 '[p1]' .then(value => { console.log(value) }) // 列印 undefined,因為上一步驟 onFulfilled 并沒有傳回值 .then(value => { console.log(value) })
同理,回調函數所抛出的錯誤将作為下一個 onRejected 的參數傳遞下去:
new Promise(resolve => resolve()) .then(() => { throw Error('抛錯') }) // 列印 '抛錯' .catch(error => { console.log(error.message) })
這裡存在一個有意思的地方,由于回調函數可以傳回任何結果,是以傳回一個 Promise 對象也是可行的。
我們分别用 p1 和 p2 來分别指代 .then() 傳回的 Promise 和回調函數的 Promise,在這種情況下首先明确 p1 與 p2 是兩個不同的 Promise 對象,但是 它們的狀态是一緻的,這裡的“一緻”包括最終的狀态、狀态觸發的時機以及傳回值的一緻性。我們來舉例說明這個過程:
let p1 = new Promise(resolve => { resolve('[p1]')})let p2 = new Promise(resolve => { resolve(p1)})// 列印 falseconsole.log(p1 === p2)// 列印 “[p1]”p2.then(value => { console.log(value)})
有了這個機制之後,我們就可以将多個異步過程接到鍊上去順序執行了!
// 步驟 0,傳回 p0(fulfilled)new Promise(resolve => { console.log('0s') setTimeout(() => { resolve('+1s') }, 1000)})// 步驟 1,傳回 p1(rejected).then(value => { console.log(value) return new Promise((resolve, reject) => { reject('+2s') }, 2000)})// 步驟 2,傳回 p2(fulfilled).catch(error => { console.log(error) return new Promise(resolve => { resolve('+1s') }, 1000)})// 步驟 3,傳回 p3(fulfilled).then(value => { console.log(value)})
執行結果如下:
#(第 0 秒)0s# (第 1 秒)+1s# (第 3 秒)+2s#(第 4 秒)+1s
就這樣,我們在一個循序漸進的過程當中分析完了 Promise 鍊式調用的過程。是不是非常的簡單?←_←
總結
事實上我們在使用 Promise 的時候,并不需要過多的了解這個鍊式調用的過程也是能夠正常使用的,這是因為這其中的特性和約定,都非常的自然并且符合我們正常的思維方式,直到我們把它拎出來進行分析才發現:哇,Promise 幫我們做了那麼多事情。
除了鍊式調用之外,Promise 還在狀态可靠性方面做了不少工作,感興趣的朋友可以到《PWA 應用實戰》當中去進一步學習。
本文内容主要來自開源書籍《PWA 應用實戰》。該書由百度 Web 生态團隊撰寫與分享,記錄了團隊過去兩年積累的 PWA 方面的經驗,歡迎對 Web 和 PWA 有濃厚興趣的讀者加入我們,一起來維護這本書。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SO1ATN4cDM0ATY3U2YmBTYyEjY3YGN1UWZyATNjN2Nz8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
Brilliant Open Web
BOW(Brilliant Open Web)團隊,是一個專門的Web技術建設小組,緻力于推動 Open Web 技術的發展,讓Web重新成為開發者的首選。
BOW 關注前端,關注Web;剖析技術、分享實踐;談談學習,也聊聊管理。
關注 OpenWeb開發者,讓我們一起推動 OpenWeb技術的發展!
OpenWeb開發者
ID:BrilliantOpenWeb
技術連接配接世界,開放赢得未來