ES6+ Promise 基礎
1. 前言
我們知道瀏覽器在渲染網頁時,會創建一個渲染進程進行渲染頁面,在渲染進程中其中有 GUI 渲染線程和 JS 引擎線程(如 V8 引擎)兩個線程是互斥的。也就是說在同一時間內只能有一個線程執行。如果 JavaScript 執行一段耗時程序時會阻止頁面渲染。如果要頁面快速在用戶面前呈現就要做一些優化處理。對于不能立馬得到結果的程序,不需要等待,可以放到事件隊列中,等到得到結果后再執行。
對這種不等待方式,JavaScript 提供了異步的解決方案,在 JavaScript 中常見的異步解決方案是 Callback 方式,而像 setTimeout 這樣提供異步的 API,還可以使用發布訂閱的來實現異步。使用回調函數存在回調地獄的問題。為了解決回調地獄,最早社區提出了 Promise 概念。最后在 ES6 時正式作為官方的解決方案,說明 Promise 有它獨有的優勢。本節我們將學習 Promise 的基本用法。
2. 回調地獄
我們都知道 JavaScript 異步使用的是回調函數,下面我們來看一個 ajax 請求的實例,下面的 ajax 方法是一個偽代碼,可以看作是請求接口的方法,接口請求的庫可以參考 jQuery 的 $.ajax 和 axios。
// ajax請求的偽代碼
function ajax(url, sucessCallback, failCallback) {
// url:請求的url
// sucessCallback:成功的回調函數
// failCallback:失敗的回調函數
}
ajax(url1, (res1) => {
ajax(url2, (res2) => {
ajax(url3, (res3) => {
doSomething(res1, res2, res3)
})
})
})
上面的 ajax 請求我們可以理解為,在調用 doSomething 方法時需要前面三個請求的結果作為參數,所以只有前一個 ajax 請求得到結果后才能發起第二個請求。這樣前后有依賴的嵌套被稱為回調地獄。對于比較復雜邏輯的情況來說,回調地獄會使程序出問題的概率大大增加。
另外,這樣做有個很嚴重的問題,就是接口請求的時間是三個請求的和,不能進行并發操作,當然我們也可以做一些優化操作,如下:
let out = after(3, function (data){
doSomething(...data)
})
ajax(url1, (res1) => {
out(res1)
})
ajax(url2, (res2) => {
out(res2)
})
ajax(url3, (res3) => {
out(res3)
})
function after(times, callback) {
const arr = [];
return function (value){
arr.push(value);
if (--times==0) {
callback(arr);
}
}
}
上面的代碼很優雅地解決了回調嵌套的問題,但同時我們需要手動維護一個計數器來控制最后的回調。這無疑增加了程序的復雜度,我們更希望的是關注我的業務,而不是寫更多的邏輯來優化。
針對這種情況,社區提供了很多這類優化的庫,而 Promise 則是其中最亮眼的。對上面的情況,Promise 怎么解決的呢?看如下的實現方式:
function request(url) {
return new Promise((resolve, reject) => {
ajax(url, (res) => {
resolve(res)
})
})
}
Promise.all([request(url1), request(url1), request(url1)]).then((result) => {
doSomething(...result)
}).catch((error) => {
console.log(error)
})
上面的代碼中我們封裝了一個 request 請求的方法,通過 Promise.all()
來并發請求這些接口,當接口都正確返回才會執行 then 方法中的回調,有一個錯誤都會拋出異常。這種方式比較好的是,我們對請求進行了封裝,不要再關注每一步請求是否完成做對應的邏輯處理,讓我們在開發過程中更加關注業務邏輯,使開發效率更快。
3. Promise 用法
前面我們通過一個回調地獄的案例,說明了 Promise 的優點,就是為了解決異步而產生的。并且可以處理并發請求,很好地優化了程序資源。
3.1 實例化一個 Promise
首先需要明確 Promise 是一個類,我們在 VSCode 中輸入 new Promise()
會給我們如下的提示:
在 new Promise()
時需要默認需要傳入一個回調函數,這個回調函數是 executor(執行器),默認會立即執行。執行器會提供兩個方法(resolve 和 reject)用于改變 promise 的狀態。resolve
會觸發成功狀態,reject
會觸發失敗狀態,無論成功或失敗都會傳入一個返回值,這個返回值會在實例調用 then
方法后作為響應值獲取。
var promise = new Promise((resolve, reject) => {
ajax(url, (data) => {
resolve(data) // 成功
}, (error) => {
reject(error) // 失敗
})
})
上面的代碼中實例化一個 ajax 請求的 Promise, 當接口請求成功就會調用 resolve () 方法把請求的值傳入進去,如果失敗了就調用 reject () 方法把錯誤信息傳入進去。在后續的鏈式調用中獲取相應的結果。
我們需要知道的是,Promise 有三個狀態:等待(pending)、成功(fulfilled),失敗(rejected)。在初始化時,這個狀態是等待態,在等待狀態時可以轉化為成功態或失敗態。當狀態是成功態或是失敗態后不能再被改變了。
上面的代碼中可以改變 Promise 狀態的是執行器提供的 resolve 和 reject,resolve 會將等待態變為成功態,reject 則會將等待態變為失敗態,在狀態變為成功或失敗的狀態時就不能被更改了。
3.2 then
在實例化 Promise 類后,我們如何訪問成功和失敗的值呢?Promise 提供了鏈式調用的 then 方法用于訪問成功的值和失敗的值,then 提供了兩個回調 onfulfilled(成功回調)、onrejected(失敗回調)。
var promise = new Promise((resolve, reject) => {
resolve(123);
// reject('error')
// throw new Error('Error')
})
promise
.then(
(data) => {
console.log(data) // 123
return '100'
},
(reason) => {
console.log(reason)
}
)
.then((data) => {
console.log(data) // 100
}, null)
上面的代碼中給我了幾個測試用例,有興趣的小伙伴可以進行測試。then 方法返回一個值而不是 Promise 實例,并且會把這個結果返回到下一個 then 的成功回調中;
如果返回的是一個 promise,下一個 then 會采用這個 promise 結果,如果返回的是失敗,會傳到下一個 then 中失敗的回調函數中去:
var promise = new Promise((resolve, reject) => {
resolve(123);
})
promise
.then(
(data) => {
return new Promise((resolve, reject) => {
reject('錯誤內容');
})
},
null
)
.then(null, (err) => {
console.log('error:', err) // error: 錯誤內容
})
如果在失敗的回調函數中返回一個普通值或成功的 promise 也會走到下一層 then 的成功回調中去。
promise.then(null, (err) => {
return '100';
}).then((data) => {
console.log('data:', data); // data: 123
}, null)
通過上面的例子可以知道,當前 then 中走成功與否,主要看上一層返回的結果??偨Y有兩點。
- 當上一層返回一個普通值,或是一個成功的 Promise,則會走到下一層成功的回調函數中去;
- 如果上一層返回一個失敗的 Promise,或是拋出一個異常,則會走到下一層的失敗的回調函數中去。
4. 小結
本節主要通過 JavaScript 中回調地獄的一個案例來引出為什么使用 Promise,以及 Promise 所帶來的好處。然后學習了 Promise 的基本使用和鏈式調用 then 方法,需要注意的是,then 中執行成功或是失敗是根據它上一層的返回值,如果返回的是一個普通值或成功的 Promise 則會走 then 的成功回調;如果拋出異?;蚍祷厥〉?Promise 則走 then 的失敗回調。