作者 | 周浪
一、前言
大家都知道JavaScript一大特點就是單執行緒,為了不阻塞主執行緒,有些耗時操作(比如ajax)必須放在任務佇列中非同步執行。傳統的非同步程式設計解決方案之一回撥,很容易產生臭名昭著的回撥地獄問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
fs.readdir(source, function(err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function(filename, fileIndex) { console.log(filename) gm(source + filename).size(function(err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function(width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } }) |
雖然回撥地獄可以通過減少巢狀、模組化等方式來解決,但我們有更好的方案可以採取,那就是 Promise
二、含義
Promise
是一個物件,儲存著非同步操作的結果,在非同步操作結束後,會變更 Promise
的狀態,然後呼叫註冊在 then
方法上回撥函式。 ES6
原生提供了 Promise
物件,統一用法(具體可參考阮一峰的《ES6入門》)
三、實現
Promise
的使用想必大家都很熟練,可是究其內部原理,在這之前,我一直是一知半解。本著知其然,也要知其所以然的目的,開始對 Promise
的實現產生了興趣。
眾所周知, Promise
是對 Promises/A+
規範的一種實現,那我們首先得了解規範, 詳情請看Promise/A+規範(https://promisesaplus.com/),個人github上有對應的中文翻譯README.md
promise建構函式
規範沒有指明如何書寫建構函式,那就參考下 ES6
的構造方式
Promise
建構函式接受一個函式作為引數,該函式的兩個引數分別是 resolve
和 reject
resolve
函式的作用是將 Promise
物件的狀態從 pending
變為 fulfilled
,在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞給註冊在 then
方法上的回撥函式(then方法的第一個引數); reject
函式的作用是將 Promise
物件的狀態從 pending
變為 rejected
,在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞給註冊在 then
方法上的回撥函式(then方法的第二個引數)
所以我們要實現的 promise
(小寫以便區分ES6的 Promise
)建構函式大體如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// promise 建構函式 function promise(fn) { let that = this that.status = 'pending' // 儲存promise的state that.value = '' // 儲存promise的value that.reason = '' // 儲存promise的reason that.onFulfilledCb = [] // 儲存then方法中註冊的回撥函式(第一個引數) that.onRejectedCb = [] // 儲存then方法中註冊的回撥函式(第二個引數) // 2.1 function resolve(value) { // 將promise的狀態從pending更改為fulfilled,並且以value為引數依次呼叫then方法中註冊的回撥 setTimeout(() => { if (that.status === 'pending') { that.status = 'fulfilled' that.value = value // 2.2.2、2.2.6 that.onFulfilledCb.map(item => { item(that.value) }) } }, 0) } function reject(reason) { // 將promise的狀態從pending更改為rejected,並且以reason為引數依次呼叫then方法中註冊的回撥 setTimeout(() => { if (that.status === 'pending') { that.status = 'rejected' that.reason = reason // 2.2.3、2.2.6 that.onRejectedCb.map(item => { item(that.reason) }) } }, 0) } fn(resolve, reject) } |
規範2.2.6中明確指明 then
方法可以被同一個 promise
物件呼叫,所以這裡需要用一個陣列 onFulfilledCb
來儲存then方法中註冊的回撥
這裡我們執行 resolve
reject
內部程式碼使用setTimeout,是為了確保 then
方法上註冊的回撥能非同步執行(規範3.1)
then方法
promise
例項具有 then
方法,也就是說, then
方法是定義在原型物件 promise.prototype
上的。它的作用是為 promise
例項新增狀態改變時的回撥函式。
規範2.2
promise
必須提供一個then
方法promise.then(onFulfilled,onRejected)
規範2.2.7then
方法必須返回一個新的promise
閱讀理解規範2.1和2.2,我們也很容易對then方法進行實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
promise.prototype.then = function(onFulfilled, onRejected) { let that = this let promise2 // 2.2.1、2.2.5 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v onRejected = typeof onRejected === 'function' ? onRejected : r => r if (that.status === 'pending') { // 2.2.7 return promise2 = new promise((resolve, reject) => { that.onFulfilledCb.push(value => { try { let x = onFulfilled(value) } catch(e) { // 2.2.7.2 reject(e) } }) that.onRejectedCb.push(reason => { try { let x = onRejected(reason) } catch(e) { // 2.2.7.2 reject(e) } }) }) } } |
重點在於對 onFulfilled
、 onRejected
函式的返回值x如何處理,規範中提到一個概念叫 PromiseResolutionProcedure
,這裡我們就叫做Promise解決過程
Promise 解決過程是一個抽象的操作,需要輸入一個 promise
和一個值,我們表示為 [[Resolve]](promise,x)
,如果 x
有 then
方法且看上去像一個 Promise
,解決程式即嘗試使 promise
接受 x
的狀態;否則用 x
的值來執行 promise
promise解決過程
對照規範2.3,我們再來實現 promise resolution
, promise resolution
針對x的型別做了各種處理:如果 promise
和 x
指向同一物件,以 TypeError
為 reason
拒絕執行 promise
、如果 x
為 promise
,則使 promise
接受 x
的狀態、如果 x
為物件或者函式,判斷 x.then
是否是函式、 如果 x
不為物件或者函式,以 x
為引數執行 promise
(resolve和reject引數攜帶promise2的作用域,方便在x狀態變更後去更改promise2的狀態)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// promise resolution function promiseResolution(promise2, x, resolve, reject) { let then let thenCalled = false // 2.3.1 if (promise2 === x) { return reject(new TypeError('promise2 === x is not allowed')) } // 2.3.2 if (x instanceof promise) { x.then(resolve, reject) } // 2.3.3 if (typeof x === 'object' || typeof x === 'function') { try { // 2.3.3.1 then = x.then if (typeof then === 'function') { // 2.3.3.2 then.call(x, function resolvePromise(y) { // 2.3.3.3.3 if (thenCalled) return thenCalled = true // 2.3.3.3.1 return promiseResolution(promise2, y, resolve, reject) }, function rejectPromise(r) { // 2.3.3.3.3 if (thenCalled) return thenCalled = true // 2.3.3.3.2 return reject(r) }) } else { // 2.3.3.4 resolve(x) } } catch(e) { // 2.3.3.3.4.1 if (thenCalled) return thenCalled = true // 2.3.3.2 reject(e) } } else { // 2.3.4 resolve(x) } } |
完整程式碼可檢視stage-4(https://github.com/zhoulang27426405/learn-promise/blob/master/stage-4/promise-4.js)
思考
以上,基本實現了一個簡易版的 promise
,說白了,就是對 Promises/A+
規範的一個翻譯,將規範翻譯成程式碼。因為大家的實現都是基於這個規範,所以不同的 promise
實現之間能夠共存(不得不說制定規範的人才是最厲害的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function doSomething() { return new promise((resolve, reject) => { setTimeout(() => { resolve('promise done') }, 2000) }) } function doSomethingElse() { return new Promise((resolve, reject) => { setTimeout(() => { reject('ES6 promise') }, 1000) }) } this.promise2 = doSomething().then(doSomethingElse) console.log(this.promise2) |
至於 ES6
的 finally
、 all
等常用方法,規範雖然沒有制定,但是藉助 then
方法,我們實現起來也很方便stage-5(https://github.com/zhoulang27426405/learn-promise/tree/master/stage-5)
ES7
的 Async/Await
也是基於 promise
來實現的,可以理解成 async
函式會隱式地返回一個 Promise
, await
後面的執行程式碼放到 then
方法中
更深層次的思考,你需要理解規範中每一條制定的意義,比如為什麼then方法不像jQuery那樣返回this而是要重新返回一個新的promise物件(如果then返回了this,那麼promise2就和promise1的狀態同步,promise1狀態變更後,promise2就沒辦法接受後面非同步操作進行的狀態變更)、 promise解決過程
中為什麼要規定 promise2
和 x
不能指向同一物件(防止迴圈引用)
promise的弊端
promise徹底解決了callback hell,但也存在以下一些問題
1 2 3 4 5 6 7 8 9 |
延時問題(涉及到evnet loop)(http://www.ruanyifeng.com/blog/2014/10/event-loop.html)) promise一旦建立,無法取消 pending狀態的時候,無法得知進展到哪一步(比如介面超時,可以藉助race方法) promise會吞掉內部丟擲的錯誤,不會反映到外部。如果最後一個then方法裡出現錯誤,無法發現。(可以採取hack形式,在promise建構函式中判斷onRejectedCb的陣列長度,如果為0,就是沒有註冊回撥,這個時候就丟擲錯誤,某些庫實現done方法,它不會返回一個promise物件,且在done()中未經處理的異常不會被promise例項所捕獲) then方法每次呼叫都會建立一個新的promise物件,一定程度上造成了記憶體的浪費 |
總結
支援 promise
的庫有很多,現在主流的瀏覽器也都原生支援 promise
了,而且還有更好用的 Async/Await
。之所以還要花精力去寫這篇文章,道理很簡單,就是想對規範有一個更深的理解,希望看到這裡的同學同樣也能有所收穫。