Hello 你們好!我是壹甲壹!前端
相信你們不管在前端仍是後端開發工做中,都接觸並使用過 Promise ,本文將帶領你們「step-by-step」實現一個符合 Promises/A+ 規範的 Promise,同時探索 Promise 中的一些方法以及第三方擴展如何實現的。node
經過閱讀本篇文章你能夠學習到:git
promises-aplus-tests
進行規範測試Promise.all
,Promise.race
, Promise.resolve
, Promise.reject
等實現原理在正式進入正題以前,爲了更好地理解和掌握 Promise ,咱們先來介紹一些與 Promise 相關的基礎知識。github
你們應該都知道,JS 屬於單線程語言,所謂單線程,就是一次只能幹一件事,其它事情只能在後面乖乖排隊等待。面試
在瀏覽器中,頁面加載過程當中存在大量請求,當一個網絡請求遲遲沒有響應,頁面將傻傻等着,不能處理其它事情。npm
所以,JS 中設計了異步,即發送完網絡請求後就能夠繼續處理其它操做,而網絡請求返回的數據,可經過回調函數來接收處理,這樣就保證了頁面的正常運行。json
先看下面一段 Node 代碼後端
var fs = require('fs') fs.readFile('data.json', (err, data) => { console.log(data.toString()) }) 複製代碼
fs.readFile
方法的第二個參數是個函數,函數並不會當即執行,而是等到讀取的文件結果出來才執行,這是函數就是回調函數,即 callbackapi
處理多個異步請求,而且一個一個嵌套時,就容易產生回調地獄。看下面一段 Node 代碼數組
const fs = require('fs') fs.readFile('data1.json', (err, data1) => { fs.readFile('data2.json', (err, data2) => { fs.readFile('data3.json', (err, data3) => { fs.readFile('data4.json', (err, data4) => { console.log(data4.toString()) }) }) }) }) 複製代碼
使用 Promise 改寫
const fs = require('fs') const readFilePromise = (file) => { return new Promise((resolve, reject) => { fs.readFile(file, (err, data) => { if (err) { reject(err) } resolve(data) }) }) } readFilePromise('data1.json') .then(data1 => { return readFilePromise('data2.json') }).then(data2 => { return readFilePromise('data3.json') }).then(data3 => { return readFilePromise('data4.json') }).then(data4 => { console.log(data4.toString()) }).catch(err => { console.log(err) }) 複製代碼
「「思考題」:Promise 真的取代 callback 了嘛?
Promise 只是對於異步操做代碼的可讀性的一種變化,沒有改變 JS 中異步執行的本質,也沒法取代 callback 在 JS 中的存在。同時,在 Promise 中,也存在着 callback 的使用,實例的 then() 的參數分別是執行成功、失敗的函數,也就是 callback 回調函數。
本篇文章對應的項目地址: github.com/Yangjia23..…
首先,Promise 是個類,須要使用 new 來建立實例
new Promise((resolve, reject) => {})
傳入的參數是個函數,被稱爲 executor
執行器,默認會當即執行executor
執行時會傳入兩個參數 resolve, reject
,分別是執行成功函數、執行失敗函數resolve, reject
兩個執行函數不屬於 Promise 類上的靜態屬性,也不是實例上的方法,而是一個普通函數class Promise { constructor (executor) { // 成功 const resolve = () => {} // 失敗 const reject = () => {} // 當即執行 executor(resolve, reject) } } 複製代碼
關於 Promise 狀態
promise 有三種狀態:等待 (pending)、已成功 (fulfilled)、已失敗(rejected),默認狀態爲 pending
promise 的狀態只能從 pending
轉換成 fulfilled
或 rejected
兩種狀態變化
瞭解promise狀態更多內容,請查看Promises/A+規範: promise-states
以 readFilePromise 爲例
resolve
函數,傳入讀取的內容,表示執行成功,此時的狀態應是 fulfilled
成功態reject
函數,傳入失敗的緣由,表示執行失敗,此時的狀態應是 fulfilled
失敗態value
和 reason
存儲const ENUM = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class Promise { constructor (executor) { this.status = ENUM.PENDING // 默認狀態 this.value = undefined // 保存執行成功的值 this.reason = undefined // 保存執行失敗的緣由 // 成功 const resolve = (value) => { if (this.status === ENUM.PENDING) { this.status = ENUM.FULFILLED this.value = value } } // 失敗 const reject = (reason) => { if (this.status === ENUM.PENDING) { this.status = ENUM.REJECTED this.reason = reason } } // 當即執行 executor(resolve, reject) } } 複製代碼
因爲 executor
執行器是由用戶傳入的,在執行過程當中可能出現錯誤,此時須要使用 try...catch...
進行異常捕獲,當發生錯誤後,直接調用 reject 拋出錯誤
class Promise { constructor (executor) { // .... // 異常捕獲 try{ // 當即執行 executor(resolve, reject) } catch (e) { reject(e) } } } 複製代碼
調用 new Promise()
返回的實例上有個 then
方法,then
方法須要用戶提供兩個參數,分別是執行成功後對應的成功回調 onFulfilled
和執行失敗後對應的失敗回調 onRejected
onFulfilled
方法,並傳入成功的值 this.valueonRejected
方法,並傳入失敗的緣由 this.reasonclass Promise { constructor(executor) { // ... } then(onFulfilled, onRejected) { if (this.status == ENUM.FULFILLED) { onFulfilled(this.value) } if (this.status == ENUM.REJECTED) { onRejected(this.reason) } } } 複製代碼
當 executor
中執行的是異步操做時,執行 then
方法時狀態仍是 pending
「異步操做例如
setTimeout
屬於宏任務,而promise.then
屬於微任務, 微任務先於宏任務執行,因此then
方法執行時,promise
的狀態仍是pending
同時實例promise能夠屢次調用 then 方法,因此,須要將全部 then
方法中的回調函數蒐集保存好,當異步操做完成後,再執行保存的回調函數(基於發佈訂閱模式)
const promise = new Promise((resolve, reject) => { setTimeout(() => {}, 2000) }) promise.then(data => {//...}, err => {}) promise.then(data => {//...}, err => {}) 複製代碼
因此,接下來須要實現的是
onResolvedCallbacks
和 onRejectedCallbacks
,分別存放 then 方法中對應的成功回調和失敗回調resolve
函數時,執行 onResolvedCallbacks
隊列中每一個成功回調reject
函數時,執行 onRejectedCallbacks
隊列中每一個失敗回調class Promise { constructor(executor) { this.status = ENUM.PENDING this.value = undefined this.reason = undefined this.onResolvedCallbacks = [] // 成功隊列 this.onRejectedCallbacks = [] // 失敗隊列 // 成功回調 const resolve = (value) => { if (this.status === ENUM.PENDING) { this.status = ENUM.FULFILLED this.value = value this.onResolvedCallbacks.forEach(cb => cb()) // 相對於發佈 } } // 失敗回調 const reject = (reason) => { if (this.status === ENUM.PENDING) { this.status = ENUM.REJECTED this.reason = reason this.onRejectedCallbacks.forEach(cb => cb()) } } // 當即執行 executor(resolve, reject) } then(onFulfilled, onRejected) { // ... if (this.status === ENUM.PENDING) { // 相對於訂閱 this.onResolvedCallbacks.push(() => { // todo... onFulfilled(this.value) }); this.onRejectedCallbacks.push(() => { // todo... onRejected(this.reason); }) } } } 複製代碼
注意:在 then
方法中,並無往隊列中直接插入回調函數, 而是使用函數包裝後再 push
,是爲了方便後續擴展 ( eg:獲取並處理 onFulfilled()
的返回值)
到如今爲止,實現了基礎版 Promise , 但看着和以前的 callback 只是寫法上不一樣,並無體現出 Promise 的優點,接下來,繼續探索 Promise 中的高級特性
對於實例上的 then(onFulfilled, onRejected)
方法,其參數爲成功、失敗兩個回調函數。總結出如下幾個使用場景
then
中then
的失敗回調中捕獲異常then
的成功回調;狀態爲「失敗」則會調用下一個 then
的失敗回調)then
中拋錯或返回一個失敗的 promise ),該錯誤會被最近的一個失敗回調捕獲,當該失敗回調執行後,能夠繼續調用 then
方法在 Promise 中,promise.then 鏈式調用的實現原理是經過返回一個新的 promise 來實現的
「「思考題」爲何返回新的 promise, 而不是使用原來的 promise?
由於 promise 的狀態一旦"成功"或"失敗"了,就不能再改變了,因此只能返回新的 promise,這樣才能夠繼續調用下一個then
中的成功/失敗回調
接下來,須要實現如下幾點
then
方法,建立一個新的 promise, 最後將這個新 promise 返回then
方法中 onFulfilled
、onRejected
回調函數的返回值,經過新的 promise
傳遞到下一個 then
方法中class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => {}) if (this.status == ENUM.FULFILLED) { let x = onFulfilled(this.value) } if (this.status == ENUM.REJECTED) { let x = onRejected(this.reason) } if (this.status === ENUM.PENDING) { this.onResolvedCallbacks.push(() => { let x = onFulfilled(this.value) }); this.onRejectedCallbacks.push(() => { let x = onRejected(this.reason); }) } return promise2 } } 複製代碼
如今,須要將回調函數執行的返回值 x 傳遞到下一個 then
方法中,是傳遞到下一個 then
方法中的成功回調,仍是失敗回調?須要根據 x 的值來判斷。
resolve
傳遞給成功回調;reject
傳遞給失敗回調;由於須要使用 promise2 中的 resolve
, reject
傳遞 x (兩個方法在外部沒法獲取到), 同時new Promise(executor)
時,executor
是當即執行,因此,將整個 then
方法中的邏輯放到 executor
函數中執行,就能夠訪問到 resolve
, reject
方法了
class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => { if (this.status == ENUM.FULFILLED) { // onFulfilled 執行可能報錯,使用 try...catch...捕獲 try{ let x = onFulfilled(this.value) resolve(x) } catch (e){ reject(e) } } // ... }) return promise2 } } 複製代碼
由於返回值 x 存在多種狀況, 因此將判斷邏輯抽離到外部函數 resolvePromise 中
class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => { if (this.status == ENUM.FULFILLED) { try{ let x = onFulfilled(this.value) resolvePromise(x, promise2, resolve, reject) } catch (e){ reject(e) } } // ... }) return promise2 } } const resolvePromise = (x, promise2, resolve, reject) => { } 複製代碼
相信仔細的小夥伴已經發現,在 new Promise
還沒結束就訪問 promise2 確定會報錯。只需將 resolvePromise
變成異步代碼執行就能夠訪問到 promise2
//... if (this.status == ENUM.FULFILLED) { setTimeout(() => { try { let x = onFulfilled(this.value) resolvePromise(x, promise2, resolve, reject) } catch (e) { reject(e) } }, 0) } 複製代碼
接下來,須要實現 resolvePromise 方法了
resolvePromise 方法主要是用來解析 x 是不是promise, 按照 Promises/A+規範: the-promise-resolution-procedure 規定,分紅如下幾步
函數參數 resolvePromise(x, promise2, resolve, reject)
let promise = new Promise((resolve, reject) => {}) let promise2 = promise.then(() => { return promise2 // x 表明了then中函數的返回值,也就是 promise2 }) promise2.then(() => {}, err=> { console.log('err:', err) }) // err: TypeError: Chaining cycle detected for promise #<Promise> (循環引用了) 複製代碼
resolve
返回then
方法,當存在 then
方法,代表 x 就是一個 promise,此時執行 then
方法then
方法時,有一個成功回調和一個失敗回調,執行成功走成功回調,並傳入成功結果 y;執行失敗走失敗回調,並傳入失敗緣由 e, 使用 reject
返回then
的回調函數只能執行一次,要麼成功,要麼失敗(設置標識符 called)then
方法時,代表 x 是普通的對象,直接經過 resolve
返回const resolvePromise = (x, promise2, resolve, reject) => { // (1) if (x === promise2) { reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise>`)) } if ((typeof x === 'object' && x !== null) || typeof x === 'function') { let called = false // (6) try { const then = x.then // (3) if (typeof then === 'function') { // (4) then.call(x, y => { // (5) y 多是個 promise if (called) return called = true resolvePromise(y, promise2, resolve, reject) }, e => { if (called) return called = true reject(e) }) } else { // (7) resolve(x) } } catch (e) { // then 執行過程出錯,也不能繼續向下執行 if (called) return called = true reject(e) } } else { // (2) resolve(x) } } 複製代碼
如今 resolvePromise 方法已經基本實現,其中還有如下幾點須要說明
由於 resolvePromise 須要兼容其餘人寫的 promise , 別人的 promise 可能就是一個函數
const then = x.then
爲啥須要使用 try...catch...
捕獲異常 ?由於可使用 Object.defineProperties
或 Proxy
改寫 x.then 的返回值
then
方法,爲啥使用 call
, 而不是直接執行 x.then()
?能夠複用上次取出來的then
方法,避免二次調用 x.then()
new Promise((resolve, reject) => { resolve(123) }).then().then().then(data => { console.log('success:', data) }) // success: 123 複製代碼
上面代碼中的 123 是如何直接穿透到最後一個 then
方法中的呢?
Promises/A+規範: onFulfilled, onRejected are optional arguments , 規定 then
方法中的 onFulfilled
, onRejected
是可選參數,因此咱們須要提供一個默認值
class Promise { // ... then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v onRejected = typeof onRejected === 'function' ? onRejected: e => {throw e} // ... } } 複製代碼
經過給 onFulfilled
, onRejected
設置默認值就能夠實現值穿透。至此,已經實現 Promises/A+ 中規範的功能,能夠對代碼進行規範測試了
規範測試,首先須要安裝 promises-aplus-tests npm 包,同時須要在導出 Promise
前增長下面測試代碼
class Promise { // ... } Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } module.exports = Promise; 複製代碼
安裝依賴
npm install promises-aplus-tests -D
複製代碼
同時在 package.json 增長
"scripts": { "test": "promises-aplus-tests ./index.js" }, 複製代碼
最後,運行 npm run test
就能夠進行測試了,測試結果以下
下面介紹的內容,並非 Promises/A+ 中的規範,但咱們也能夠繼續探索
實例上的 catch
方法用來捕獲執行過程當中產生的錯誤,同時返回值爲 promise, 參數爲一個失敗回調函數,相對於執行 then(null, onRejected)
class Promise{ // ... catch (onErrorCallback) { return this.then(null, onErrorCallback) } } 複製代碼
finally
的參數是一個回調函數,不管 promise 是執行成功,仍是失敗,該回調函數都會執行。
應用場景有:頁面異步請求數據,不管數據請求成功仍是失敗,在 finally 回調函數中都關閉 loading。
同時,finally
方法有如下特色
then
方法中,或者將錯誤傳遞到下一個 catch
方法中finally
回調函數返回一個新的 promise, finally
會等待該 promise 執行結束後才處理傳值finally
方法將不予理會執行結果,仍是將上一個的結果傳遞到下一個 then
中finally
方法會將錯誤緣由傳遞到下一個 catch
方法下面是具體代碼演示
// (1) 值穿透, 請注意 finally 的回調函數是不存在參數的 Promise.resolve(100).finally((data) => { console.log('finally: ', data) }).then(data => { console.log('success: ', data) }).catch(err => { console.log('error', err) }) // finally: undefined // success: 100 // (2) 等待執行 // 返回一個執行成功的 promise, 但向下傳遞但仍是上一次執行結果 Promise.resolve(100).finally(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 1000) }) }).then(data => { console.log('success: ', data) // success: 100 }).catch(err => { console.log('error', err) }) // 當 promise 執行失敗,則將該 promise 執行結果向下傳遞 Promise.reject(100).finally(() => { return new Promise((resolve, reject) => { setTimeout(() => { reject(200) }, 1000) }) }).then(data => { console.log('success: ', data) }).catch(err => { console.log('error', err) // error 200 }) 複製代碼
在掌握了 finally
的用法後,繼續探索如何實現它?
class Promise{ finally (callback) { return this.then(value => { return Promise.resolve(callback()).then(() => value) }, err => { return Promise.resolve(callback()).then(() => {throw err}) }) } } 複製代碼
靜態方法是那經過 Promise 來調用,而不是經過實例 promise 來調用的方法
class Promise{ // ... // 成功狀態 static resolve(value){ return new Promise((resolve, reject) => { resolve(value) }) } // 失敗狀態 static reject(reason){ return new Promise((resolve, reject) => { reject(reason) }) } } 複製代碼
假設執行成功返回值 value
是個 promise,Promise.resolve() 會對該 value 遞歸解析,直到該 promise 執行結束纔會向下執行
class Promise{ constructor() { //... const resolve = (value) => { if (value instanceof Promise) { // 遞歸解析, 直到 value 爲普通值 value.then(resolve, reject) } // ... } const reject = (err) => { // ... } //... } } 複製代碼
如今,執行下面代碼,就能夠正常獲取數據了
Promise.resolve(new Promise((resolve, reject) => { setTimeout(() => { resolve('hello') }, 2000) })).then(data => { console.log(data) // hello }) 複製代碼
解決併發問題,多個異步併發並獲取最終的結果。
參數是一個 promise數組,當數組中每一項都執行成功,結果就是成功,反之,有一個失敗,結果就是失敗。
class Promise { static all(arrList) { if (!Array.isArray(arrList)) { const type = typeof arrList; return new TypeError(`TypeError: ${type} ${arrList} is not iterable`) } return new Promise((resolve, reject) => { const backArr = [] const count = 0 const processResultByKey = (value, index) => { backArr[index] = value if (++count === arrList.length) { resolve(backArr) } } for (let i = 0; i < arrList.length; i++) { const item = arrList[i]; if (item && item.then === 'function') { item.then((value) => { processResultByKey(value, i) }, reject) } else { processResultByKey(item, i) } } }) } } 複製代碼
⚠️注意:在 all
方法中,是經過 ++count === arrList.length
(count 爲計數器) 來判斷是否所有執行完成,而不是使用 index === arrlist.length - 1
來判斷,具體緣由以下
// p1 爲 promise 實例 Promise.all([1,2, p1, 4]).then(data => {}) // 當執行數組最後一項時,index === arrlist.length - 1 表達式成立, // 就會執行 resolve 返回執行結果, // 但此時的 p1 可能還沒執行結束,因此使用計數器來判斷 複製代碼
跟 all
方法不一樣的是,Promise.race 採用最早成功或最早失敗的做爲執行結果
class Promise { static race(arrList) { return new Promise((resolve, reject) => { for (let i = 0; i < arrList.length; i++) { const value = arrList[i]; if (value && value.then === 'function') { value.then(resolve, reject) } else { resolve(value) } } }) } } 複製代碼
Promise.race 的主要應用場景以下
promise
的執行 (異步請求設置超時時間,當超時後,異步請求就會被迫失敗)原生的 promise 上並無 abort
(中止、中斷) 方法,假設使用場景以下
const p1 = new Promise((resolve, reject) => { setTimeout(() => { // 模擬異步請求,5s 後返回 resolve('hello') }, 5000) }) const newP = wrap(p1) setTimeout(() => { // 設置超時時間,超時後,調用 newP.abort newP.abort('請求超時了') }, 4000) newP.then(data => {}).catch(err => {}) 複製代碼
newP1 是一個具備 abort
方法的 promise, 超時後就調用 newP.abort()
。
如今須要實現 wrap
封裝方法,傳入一個普通 promise 實例,返回一個具備 abort
方法的 promise 實例
const wrap = (promise) => { let abort let newPromise = new Promise((resolve, reject) => { abort = reject }) let p = Promise.race([promise, newPromise]) p.abort = abort return p } 複製代碼
wrap
方法就是利用 Promise.race 採用最快的做爲執行結果這一特性,來看 promise, newPromise
哪一個最早執行,而 newPromise
的執行,是經過外部調用 abort 來實現的
「⚠️注意:如下對 Promise 的擴展僅適用於 Node 環境
功能:把 node 中的一個 api 轉換成promise的寫法, 以 fs.readFile
讀取文件爲例
常規寫法
const fs = require('fs) fs.readFile('./name.json', (err, data) => {}) 複製代碼
缺點:回調地獄嵌套
改爲 promisify 鏈式調用寫法
const util = require('util') const read = util.promisify(fs.readFile) read('./name.json').then(data => console.log(data)) 複製代碼
特色:promisify 方法特色以下
const promisify = fn => { return (...args) => { return new Promise((resolve, reject) => { fn(...args, (err, data) => { if (err) reject(err) resolve(data) }) }) } } 複製代碼
promisify
方法每次只能修改一個方法,而第三方的庫 bluebird 中實現了 promisifyAll
方法,能夠將某個對象下全部的方法轉換成 promise 寫法
const fs = require('fs') const bluebird = require('bluebird'); // 第三方庫,需提早安裝 const newFs = bluebird.promisifyAll(fs); newFs.readFileAsync('./name.txt', 'utf-8').then(data => {}).catch(err => {}) 複製代碼
promisifyAll() 特色以下
const promisifyAll = (target) { Reflect.ownKeys(target).forEach(key => { target[`${key}Async`] = promisify(target[key]) }) return target } 複製代碼
Reflect 對象是 ES 中內置對象,它提供攔截 JavaScript 操做的方法 Reflect | MDN, 此處,也可以使用 Object.keys()
。同時,使用了前面的 promisify
來改寫方法
目前,在高版本瀏覽器中,已經對 api 集成了 promise 的寫法,使用以下
const fs = require('fs').promises fs.readFile('./name.txt', 'utf-8').then(data => {}) 複製代碼
正由於原生的支持,致使第三方的一些擴展再也不流行
本文使用 mdnice 排版