今天寫的這篇文章是關於 Promise 的,其實在谷歌一搜索,會出來不少關於Promise的文章,那爲何還要寫這篇文章呢?git
我相信必定有人用過 Promise ,但總有點似懂非懂的感受,好比咱們知道異步操做的執行是經過 then 來實現的,那後面的操做是如何得知前面異步操做完成的呢? Promise 具體是怎樣實現的呢?github
因此我寫這篇文章的目的主要是從最基礎的點開始剖析,一步一步來理解 Promise 的背後實現原理編程
也是由於最近本身的困惑,後面邊看文章,邊調試代碼,以致於對 Promise 的理解又上升了一個臺階~數組
咱們能夠想象這樣一種應用場景,須要連續執行兩個或者多個異步操做,每個後來的操做都在前面的操做執行成功以後,帶着上一步操做所返回的結果開始執行promise
在過去,咱們會作多重的異步操做,好比緩存
doFirstThing((firstResult) => {
doSecondThing(firstResult, (secondResult) => {
console.log(`The secondResult is:` + secondResult)
})
})
複製代碼
這種多層嵌套來解決一個異步操做依賴前一個異步操做的需求,不只層次不夠清晰,當異步操做過多時,還會出現經典的回調地獄異步
那正確的打開方式是怎樣的呢?Promise 提供了一個解決上述問題的模式,咱們先回到上面那個多層異步嵌套的問題,接下來轉變爲 Promise 的實現方式:async
function doFirstThing() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('獲取第一個數據')
let firstResult = 3 + 4
resolve(firstResult)
},400)
})
}
function doSecondThing(firstResult) {
console.log('獲取第二個數據')
let secondResult = firstResult * 5
return secondResult
}
doFirstThing()
.then(firstResult => doSecondThing(firstResult))
.then(secondResult => {
console.log(`The secondResult Result: ${secondResult}`
)})
.catch(err => {
console.log('err',err)
})
複製代碼
能夠看到結果就是咱們預期獲得的,須要注意的一點是,若是想要在回調中獲取上個 Promise 中的結果,上個 Promise 中必須有返回結果ide
相信通過上面的應用場景,已經大體明白 Promise 的做用了,那它的具體定義是什麼呢?異步編程
Promise 是對異步編程的一種抽象,是一個代理對象,表明一個必須進行異步處理的函數返回的值或拋出的異常
簡單來講,Promise 主要就是爲了解決異步回調的問題,正如上面的例子所示
能夠將異步對象和回調函數脫離開來,經過 then 方法在這個異步操做上面綁定回調函數
用 Promise 來處理異步回調使得代碼層析清晰,便於理解,且更加容易維護,其主流規範目前主要是 Promises/A+ ,下面介紹具體的API
Promise 有3種狀態: pending
(待解決,這也是初始狀態), fulfilled
(完成), rejected
(拒絕)
狀態只能由
pending
變爲fulfilled
或由pending
變爲rejected
,且狀態改變以後不會再發生變化,會一直保持這個狀態
Promise 的值是指狀態改變時傳遞給回調函數的值
Promise 惟一接口 then
方法,它須要2個參數,分別是 onResolved
和 onRejected
而且須要返回一個 promise 對象來支持鏈式調用
Promise 的構造函數接收一個函數參數,參數形式是固定的異步任務,接收的函數參數又包含 resolve
和 reject
兩個函數參數,能夠用於改變 Promise 的狀態和傳入 Promise 的值
resolve:將 Promise 對象的狀態從 pending
(進行中)變爲 fulfilled
(已成功)
reject:將 Promise 對象的狀態從 pending
(進行中)變爲 rejected
(已失敗)
resolve 和 reject 均可以傳入任意類型的值做爲實參,表示 Promise 對象成功( fulfilled
)和失敗( rejected
)的值
瞭解了 Promise 的狀態和值,接下來,咱們開始講解 Promise 的實現步驟
咱們已經瞭解到實現多個相互依賴異步操做的執行是經過 then 來實現的,那從新回到最開始的疑問,後面的操做是怎麼得知異步操做完成了呢?
在講解 Promise 實現以前,咱們仍是先簡要提一下Vue的發佈/訂閱模式:首先有一個事件數組來收集事件,而後訂閱經過 on 將事件放入數組, emit 觸發數組相應事件
那 Promise 呢? Promise 內部其實也有一個數組隊列存放事件, then 裏邊的回調函數就存放數組隊列中。下面咱們能夠看下具體的實現步驟
( demo1 )
class Promise {
constructor (executor) {
this.value = undefined
this.status = 'pending'
executor(value => {
this.status = 'resolve',
this.value = value
}, reason => {
this.status = 'rejected'
this.value = reason
})
}
then(onResolved) {
onResolved(this.value)
}
}
// 測試
var promise = new Promise((resolve, reject) => {
resolve('promise')
})
promise.then(value => {
console.log('value',value)
})
promise.then(value => {
console.log('value',value)
})
複製代碼
上述代碼很簡單,大體的邏輯是:
經過構造器 constructor
定義 Promise 的初始狀態和初始值,經過 Promise 的構造函數接收一個函數參數 executor
, 接收的函數參數又包含 resolve
和 reject
兩個函數參數,能夠用於改變 Promise 的狀態和傳入 Promise 的值。
而後調用 then 方法,將 Promise 操做成功後的值傳入回調函數
相信有人會好奇,上述 Promise 實例中都是進行的同步操做,可是每每咱們使用 Promise 都是進行的異步操做,那會出現怎樣的結果呢?在上述例子上進行修改,咱們用 setTimeout 來模擬異步的實現
// 測試
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise')
},300)
})
複製代碼
會發現後面的回調函數中打印出來的值都是undefined
很明顯,這種錯誤的形成是由於 then 裏邊的回調函數在實例化 Promise 操做 resolve 或 reject 以前就執行完成了,因此咱們應該設定觸發回調函數執行的標識,也就是在狀態和值發生改變以後再執行回調函數
正確的邏輯是這樣的:
調用 then 方法,將須要在 Promise 異步操做成功時執行的回調函數放入 children 數組隊列中,其實也就是註冊回調函數,相似於觀察者模式
建立 Promise 實例時傳入的函數會被賦予一個函數類型的參數,即 resolve
( reject
),它接收一個參數 value ,當異步操做執行成功後,會調用 resolve
( reject
)方法,這時候其實真正執行的操做是將 children 隊列中的回調一一執行
在 demo1 的基礎上修改以下:
( demo2 )
class Promise {
constructor (executor) {
this.value = undefined
this.status = 'pending'
this.children = [] // children爲數組隊列,存放多個回調函數
executor(value => {
this.status = 'resolve',
this.setValue(value)
}, reason => {
this.status = 'rejected'
this.setValue(reason)
})
}
then (onResolved) {
this.children.push(onResolved)
}
setValue (value) {
this.value = value
this.children.forEach(child => {
child(this.value)
})
}
}
// 測試
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise')
},300)
})
promise.then(value => {
console.log('value',value)
})
複製代碼
首先實例化 Promise 時,傳給 promise 的函數發送異步請求,接着調用 promise 對象的 then 函數,註冊請求成功的回調函數,而後當異步請求發送成功時,調用 resolve ( rejected )方法,該方法依次執行 then 方法註冊的回調數組
相信仔細的人應該能夠看出來,then 方法應該可以支持鏈式調用,可是上面的初步實現顯然沒法支持鏈式調用
那怎樣才能作到支持鏈式調用呢?其實實現也很簡單:
then (onResolved) {
this.children.push(onResolved)
return this
}
複製代碼
// 測試
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise')
},300)
})
promise.then(value1 => {
console.log('value1',value1)
}).then(value2 => {
console.log('value2',value2)
})
複製代碼
then 方法中加入 return this
實現了鏈式調用,但若是須要在 then 回調函數中返回一個值 value 或者 promise ,傳給下一個 then 回調函數呢?
先來看返回值 value 的狀況,好比:
// 測試
promise.then(value1 => {
console.log('value1',value1)
let value = 'promise2'
return value
}).then(value2 => {
console.log('value2',value2)
})
複製代碼
在 demo2 的基礎上進行改造:
( demo3 )
then (onResolved) {
var child = new Promise(() => {})
child.onResolved = onResolved
this.children.push(child)
return this
}
setValue (value) {
this.value = value
this.children.forEach(child => {
var ret = child.onResolved(this.value)
this.value = ret
})
}
複製代碼
原理就是在調用 Promise 對象的 then 函數時,註冊全部請求成功的回調函數,後續在 setValue 函數中循環全部的回調函數,每次執行完一個回調函數就會更新 this.value
的值,而後將更新後的 this.value
傳入下一個回調函數裏,這樣就解決了傳值的問題
但這樣也會出現一個問題,咱們只考慮了串行 Promise 的狀況下依次更新 this.value
的值,若是串行和並行一塊兒呢?好比:
// 測試
// 串行
promise.then(value1 => {
console.log('value1',value1)
let value = 'promise2'
return value
}).then(value2 => {
console.log('value2',value2)
})
// 並行
promise.then(value1 => {
console.log('value1',value1)
})
複製代碼
打印出來的結果最後一個 value1 爲 undefined ,由於咱們一直在改變 this.value
的值,而且在串行最後一個 then 回調函數中也顯示設定返回值,默認返回 undefined
可見 return this
並行不通,繼續在 demo3 的基礎上改造 then 和 setValue 函數以下:
( demo4 )
then (onResolved) {
var child = new Promise(() => {})
child.onResolved = onResolved
this.children.push(child)
return child
}
setValue (value) {
this.value = value
this.children.forEach(child => {
var ret = child.onResolved(this.value)
child.setValue(ret)
})
}
複製代碼
那若是 then 回調函數中返回一個 promise 呢?好比:
// 測試
promise.then(value1 => {
console.log('value1',value1)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise2')
},200)
})
}).then(value2 => {
console.log('value2',value2)
})
複製代碼
很明顯,打印出來的結果是個 Promise 。繼續在 demo4 的基礎上改造 setValue 函數
( demo5 )
setValue (value) {
if (value && value.then) {
value.then(realValue => {
this.setValue(realValue)
})
} else {
this.value = value
this.children.forEach(child => {
var ret = child.onResolved(this.value)
child.setValue(ret)
})
}
}
複製代碼
在 setValue 方法裏面,咱們對 value 進行了判斷,若是是一個 promise 對象,就會調用其 then 方法,造成一個嵌套,直到其不是 promise 對象爲止
到目前爲止,咱們已經實現了 Promise 的主要功能—'開枝散葉',狀態和值的有序更新
上面全部列舉到的 demo 都是在異步操做成功的狀況下進行的,但異步操做不可能都成功,在異步操做失敗時,狀態爲標記爲 rejected
,並執行註冊的失敗回調
rejected
失敗的錯誤處理也相似於 resolve
成功狀態下的處理,接着在 demo5 的註冊回調、處理狀態上加入新的邏輯,在 Promise 上加入 resolve
和 reject
靜態函數
( demo6 )
class Promise {
constructor (executor) {
this.value = undefined
this.status = 'pending'
this.children = []
executor(value => {
this.setValue(value, 'resolved')
}, reason => {
this.setValue(reason, 'rejected')
})
}
then (onResolved, onRejected) {
var child = new Promise(() => {})
this.children.push(child)
Object.assign(child, {
onResolved: onResolved || (value => value),
onRejected: onRejected || (reason => Promise.reject(reason))
})
if (this.status !== 'pending') {
child.triggerHandler(this.value, this.status)
}
return child
}
catch (onRejected) {
return this.then(null, onRejected)
}
triggerHandler (parentValue, status) {
var handler
if (status === 'resolved') {
handler = this.onResolved
} else if (status === 'rejected') {
handler = this.onRejected
}
this.setValue(handler(parentValue), 'resolved')
}
setValue (value, status) {
if (value && value.then) {
value.then(realValue => {
this.setValue(realValue, 'resolved')
}, reason => {
this.setValue(reason, 'rejected')
})
} else {
this.status = status
this.value = value
this.children.forEach(child => {
child.triggerHandler(value, status)
})
}
}
static resolve (value) {
return new Promise(resolve => {
resolve(value)
})
}
static reject (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
}
複製代碼
then 函數中有兩個回調 handler, 分別是onResolved
和 onResolved
,表示成功執行的回調函數和是失敗執行的回調函數,並設置默認值,保持鏈式鏈接
定義一個 triggerHandler
函數用來判斷當前 child 的 status ,並觸發本身的 handler ,執行回調函數,再次更新 status 和 value
setValue 函數同時設置 Promise 本身的狀態和值,而後在從新設置新的狀態以後循環遍歷 children
爲了更高效率的運行,在 then 函數中註冊回調函數時加入狀態判斷,若是狀態改變不爲 pending
,說明 setValue 函數已經執行,狀態已經發生了更改,就立馬執行 triggerHandle r函數;若是狀態爲 pending
,則在 setValue 函數執行時再觸發 triggerHandle `函數
Promise/A+
規範要求 handler 執行必須是異步的, 具體能夠參見標準 3.1 條
Here 「platform code」 means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a 「macro-task」 mechanism such as setTimeout or setImmediate, or with a 「micro-task」 mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or 「trampoline」 in which the handlers are called
這裏用 setTimeout 簡單實現一個跨平臺的 nextTick
function nextTick(func) {
setTimeout(func)
}
複製代碼
而後使用 nextTick
包裹 triggerHandler
triggerHandler (status, parentValue) {
nextTick(() => {
var handler
if (status === 'resolved') {
handler = this.onResolved
} else if (status === 'rejected') {
handler = this.onRejected
}
this.setStatus('resolved', handler(parentValue))
})
}
複製代碼
在 demo6 中咱們實現了不論是異步仍是同步均可以執行 triggerHandler ,那爲何要強制異步的要求呢?
主要是爲了流程可預測,標準須要強制異步。可類比於經典的 image onload 問題
var image = new Image()
image.onload = funtion
image.src = 'url'
複製代碼
src 屬性爲何須要寫在 onload 事件後面?
由於 js 內部是按順序逐行執行的,能夠認爲是同步的,給 image 賦值 src 時,去加載圖片這個過程是異步的,這個異步過程完成後,若是有 onload ,則執行 onload
若是先賦值 src ,那麼這個異步過程可能在你賦值 onload 以前就完成了(好比圖片緩存),那麼 onload 就不會執行
反之, js 同步執行肯定 onload 賦值完成後纔會賦值 src ,能夠保證這個異步過程在 onload 賦值完成後纔開始進行,也就保證了 onload 必定會被執行到
一樣的,在Promise中,咱們但願代碼執行順序是徹底能夠預測的,不容許出現任何問題
上述 Promise 每一個階段層次的實現代碼可見個人 github 分層實現Promise
須要注意的是:
promise 裏面的 then 函數僅僅是註冊了後續須要執行的回調函數,真正的執行是在 triggerHandler 方法裏
then 和 catch 註冊完回調函數後,返回的是一個新的 Promise 對象,以延續鏈式調用
對於內部 pending 、fulfilled 和 rejected 的狀態轉變,經過 handler 觸發 resolve 和 reject 方法,而後在 setValue 中更改狀態和值