淺析Promise原理
一、Promise基礎用法
1.1 基本用法
new Promise( function( resolve, reject) { |
- 新建一個
promise
很簡單,只需要new
一個promise
物件即可。所以promise
本質上就是一個函式,它接受一個函式作為引數,並且會返回promise
物件,這就給鏈式呼叫提供了基礎- 其實
Promise
函式的使命,就是構建出它的例項,並且負責幫我們管理這些例項。而這些例項有以下三種狀態:
-
pending
: 初始狀態,位履行或拒絕 -
fulfilled
: 意味著操作成功完成 -
rejected
: 意味著操作失敗
pending
狀態的Promise
物件可能以fulfilled
狀態返回了一個值,也可能被某種理由(異常資訊)拒絕(reject
)了。當其中任一種情況出現時,Promise
物件的then
方法繫結的處理方法(handlers)就會被呼叫,then方法分別指定了resolve
方法和reject
方法的回撥函式
var promise = new Promise( function( resolve, reject) { |
上述程式碼很清晰的展示了
promise
物件執行的機制。下面再看一個示例:
var getJSON = function( url) { |
上面程式碼中,
resolve
方法和reject
方法呼叫時,都帶有引數。它們的引數會被傳遞給回撥函式。reject
方法的引數通常是Error
物件的例項,而resolve
方法的引數除了正常的值以外,還可能是另一個Promise
例項,比如像下面這樣。
var p1 = new Promise( function( resolve, reject){ |
上面程式碼中,
p1
和p2
都是Promise
的例項,但是p2
的resolve
方法將p1
作為引數,這時p1
的狀態就會傳遞給p2
。如果呼叫的時候,p1
的狀態是pending
,那麼p2
的回撥函式就會等待p1
的狀態改變;如果p1
的狀態已經是fulfilled
或者rejected
,那麼p2
的回撥函式將會立刻執行
1.2 promise捕獲錯誤
Promise.prototype.catch
方法是Promise.prototype.then(null, rejection)
的別名,用於指定發生錯誤時的回撥函式
getJSON( "/visa.json").then( function( result) { |
Promise
物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch
語句捕獲
getJSON( "/visa.json").then( function( json) { |
1.3 常用的promise方法
Promise.all方法
Promise.all
方法用於將多個Promise
例項,包裝成一個新的Promise
例項
var p = Promise.all([p1,p2,p3]); |
- 上面程式碼中,
Promise.all
方法接受一個陣列作為引數,p1
、p2
、p3
都是Promise
物件的例項。(Promise.all
方法的引數不一定是陣列,但是必須具有iterator
介面,且返回的每個成員都是Promise
例項。)
p
的狀態由p1
、p2
、p3
決定,分成兩種情況
- 只有
p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個陣列,傳遞給p
的回撥函式 - 只要
p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的例項的返回值,會傳遞給p的回撥函式
// 生成一個Promise物件的陣列 |
Promise.race方法
Promise.race
方法同樣是將多個Promise
例項,包裝成一個新的Promise
例項。
var p = Promise.race([p1,p2,p3]); |
上面程式碼中,只要
p1
、p2
、p3
之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的Promise例項的返回值,就傳遞給p的返回值
- 如果
Promise.all
方法和Promise.race
方法的引數,不是Promise
例項,就會先呼叫下面講到的Promise.resolve
方法,將引數轉為Promise
例項,再進一步處理
Promise.resolve
有時需要將現有物件轉為
Promise
物件,Promise.resolve
方法就起到這個作用
var jsPromise = Promise.resolve($.ajax( '/whatever.json')); |
上面程式碼將
jQuery
生成deferred
物件,轉為一個新的ES6
的Promise
物件
- 如果
Promise.resolve
方法的引數,不是具有then
方法的物件(又稱thenable
物件),則返回一個新的Promise
物件,且它的狀態為fulfilled
。
var p = Promise.resolve( 'Hello'); |
- 上面程式碼生成一個新的
Promise
物件的例項p
,它的狀態為fulfilled
,所以回撥函式會立即執行,Promise.resolve
方法的引數就是回撥函式的引數 - 如果
Promise.resolve
方法的引數是一個Promise
物件的例項,則會被原封不動地返回 -
Promise.reject(reason)
方法也會返回一個新的Promise
例項,該例項的狀態為rejected
。Promise.reject
方法的引數reason
,會被傳遞給例項的回撥函式
var p = Promise.reject( '出錯啦'); |
1.4 Async/await簡化寫法
function getDataAsync ( url) { |
async function getData ( ) { |
async/await
是基於Promise
的,因為使用async
修飾的方法最終返回一個Promise
, 實際上,async/await
可以看做是使用Generator
函式處理非同步的語法糖,我們來看看如何使用Generator
函式處理非同步
1.5 Generator
首先非同步函式依然是:
function getDataAsync ( url) { |
使用
Generator
函式可以這樣寫
function * getData ( ) { |
然後我們這樣逐步執行
var g = getData() |
上面的程式碼,我們逐步呼叫遍歷器的
next()
方法,由於每一個next()
方法返回值的value
屬性為一個Promise
物件,所以我們為其新增then
方法, 在then
方法裡面接著執行next
方法挪移遍歷器指標,直到Generator
函式執行完成,實際上,這個過程我們不必手動完成,可以封裝成一個簡單的執行器
function run ( gen) { |
run
方法用來自動執行非同步的Generator
函式,其實就是一個遞迴的過程呼叫的過程。這樣我們就不必手動執行Generator
函式了。 有了run
方法,我們只需要這樣執行 getData 方法
run(getData) |
這樣,我們就可以把非同步操作封裝到
Generator
函式內部,使用run
方法作為Generator
函式的自執行器,來處理非同步。其實我們不難發現,async/await
方法相比於Generator
處理非同步的方式,有很多相似的地方,只不過async/await
在語義化方面更加明顯,同時async/await
不需要我們手寫執行器,其內部已經幫我們封裝好了,這就是為什麼說async/await
是Generator
函式處理非同步的語法糖了
二、Promise實現原理剖析
2.1 Promise標準
-
Promise
規範有很多,如Promise/A
,Promise/B
,Promise/D
以及Promise/A
的升級版Promise/A+
。ES6
中採用了Promise/A+
規範
中文版規範:
Promise標準解讀
- 一個
promise
的當前狀態只能是pending
、fulfilled
和rejected
三種之一。狀態改變只能是pending
到fulfilled
或者pending
到rejected
。狀態改變不可逆 -
promise
的then
方法接收兩個可選引數,表示該promise
狀態改變時的回撥(promise.then(onFulfilled, onRejected)
)。then
方法返回一個promise
。then
方法可以被同一個promise
呼叫多次
2.2 實現Promise
建構函式
function Promise( resolver) {} |
原型方法
Promise.prototype.then = function( ) {} |
靜態方法
Promise.resolve = function( ) {} |
2.3 極簡promise雛形
function Promise( fn) { |
大致的邏輯是這樣的
- 呼叫
then
方法,將想要在Promise
非同步操作成功時執行的回撥放入callbacks
佇列,其實也就是註冊回撥函式,可以向觀察者模式方向思考 - 建立
Promise
例項時傳入的函式會被賦予一個函式型別的引數,即resolve
,它接收一個引數value
,代表非同步操作返回的結果,當一步操作執行成功後,使用者會呼叫resolve
方法,這時候其實真正執行的操作是將callbacks
佇列中的回撥一一執行
//例1 |
// 結合例子1分析 |
結合例1中的程式碼來看,首先
new Promise
時,傳給promise
的函式傳送非同步請求,接著呼叫promise
物件的then
屬性,註冊請求成功的回撥函式,然後當非同步請求傳送成功時,呼叫resolve(results.id)
方法, 該方法執行then
方法註冊的回撥陣列
-
then
方法應該能夠鏈式呼叫,但是上面的最基礎簡單的版本顯然無法支援鏈式呼叫。想讓then
方法支援鏈式呼叫,其實也是很簡單的
this.then = function ( onFulfilled) { |
只要簡單一句話就可以實現類似下面的鏈式呼叫
// 例2 |
2.4 加入延時機制
上述程式碼可能還存在一個問題:如果在
then
方法註冊回撥之前,resolve
函式就執行了,怎麼辦?比如promise
內部的函式是同步函式
// 例3 |
這顯然是不允許的,
Promises/A+
規範明確要求回撥需要透過非同步方式執行,用以保證一致可靠的執行順序。因此我們要加入一些處理,保證在resolve
執行之前,then
方法已經註冊完所有的回撥。我們可以這樣改造下resolve
函式:
function resolve( value) { |
上述程式碼的思路也很簡單,就是透過
setTimeout
機制,將resolve
中執行回撥的邏輯放置到JS
任務佇列末尾,以保證在resolve
執行時,then
方法的回撥函式已經註冊完成
- 但是,這樣好像還存在一個問題,可以細想一下:如果
Promise
非同步操作已經成功,這時,在非同步操作成功之前註冊的回撥都會執行,但是在Promise
非同步操作成功這之後呼叫的then
註冊的回撥就再也不會執行了,這顯然不是我們想要的
2.5 加入狀態
我們必須加入狀態機制,也就是大家熟知的
pending
、
fulfilled
、
rejected
Promises/A+
規範中的2.1 Promise States
中明確規定了,pending
可以轉化為fulfilled
或rejected
並且只能轉化一次,也就是說如果pending
轉化到fulfilled
狀態,那麼就不能再轉化到rejected
。並且fulfilled
和rejected
狀態只能由pending
轉化而來,兩者之間不能互相轉換
//改進後的程式碼是這樣的: |
上述程式碼的思路是這樣的:
resolve
執行時,會將狀態設定為fulfilled
,在此之後呼叫then
新增的新回撥,都會立即執行
2.6 鏈式Promise
如果使用者在
then
函式里面註冊的仍然是一個Promise
,該如何解決?比如下面的例4
// 例4 |
- 這種場景相信用過
promise
的人都知道會有很多,那麼類似這種就是所謂的鏈式Promise
- 鏈式
Promise
是指在當前promise
達到fulfilled
狀態後,即開始進行下一個promise
(後鄰promise
)。那麼我們如何銜接當前promise
和後鄰promise
呢?(這是這裡的難點 - 只要在
then
方法裡面return
一個promise
就好啦。Promises/A+
規範中的2.2.7
就是這樣
下面來看看這段暗藏玄機的
then
方法和resolve
方法改造程式碼
function Promise( fn) { |
我們結合例4的程式碼,分析下上面的程式碼邏輯,為了方便閱讀,我把例4的程式碼貼在這裡
// 例4 |
-
then
方法中,建立並返回了新的Promise
例項,這是序列Promis
e的基礎,並且支援鏈式呼叫 -
handle
方法是promise
內部的方法。then
方法傳入的形參onFulfilled
以及建立新Promise
例項時傳入的resolve
均被push
到當前promise
的callbacks
佇列中,這是銜接當前promise
和後鄰promise
的關鍵所在 -
getUserId
生成的promise
(簡稱getUserId promise
)非同步操作成功,執行其內部方法resolve
,傳入的引數正是非同步操作的結果id
- 呼叫
handle
方法處理callbacks
佇列中的回撥:getUserJobById
方法,生成新的promise
(getUserJobById promise
) - 執行之前由
getUserId promise
的then
方法生成的新promise
(稱為bridge promise
)的resolve
方法,傳入引數為getUserJobById promise
。這種情況下,會將該resolve
方法傳入getUserJobById promise
的then
方法中,並直接返回 - 在
getUserJobById promise
非同步操作成功時,執行其callbacks
中的回撥:getUserId bridge promise
中的resolve
方法 - 最後執行
getUserId bridge promise
的後鄰promise
的callbacks
中的回撥
2.7 失敗處理
在非同步操作失敗時,標記其狀態為
rejected
,並執行註冊的失敗回撥
//例5 |
有了之前處理
fulfilled
狀態的經驗,支援錯誤處理變得很容易,只需要在註冊回撥、處理狀態變更上都要加入新的邏輯
function Promise( fn) { |
上述程式碼增加了新的
reject
方法,供非同步操作失敗時呼叫,同時抽出了resolve
和reject
共用的部分,形成execute
方法
錯誤冒泡是上述程式碼已經支援,且非常實用的一個特性。在
handle
中發現沒有指定非同步操作失敗的回撥時,會直接將
bridge promise
(
then
函式返回的
promise
,後同)設為
rejected
狀態,如此達成執行後續失敗回撥的效果。這有利於簡化序列Promise的失敗處理成本,因為一組非同步操作往往會對應一個實際功能,失敗處理方法通常是一致的
//例6 |
2.8 異常處理
如果在執行成功回撥、失敗回撥時程式碼出錯怎麼辦?對於這類異常,可以使用
try-catch
捕獲錯誤,並將bridge promise
設為rejected
狀態。handle
方法改造如下
function handle( callback) { |
如果在非同步操作中,多次執行
resolve
或者reject
會重複處理後續回撥,可以透過內建一個標誌位解決
2.9 完整實現
// 三種狀態 |
2.10 小結
這裡一定要注意的點是:
promise
裡面的
then
函式僅僅是註冊了後續需要執行的程式碼,真正的執行是在
resolve
方法裡面執行的,理清了這層,再來分析原始碼會省力的多
現在回顧下
Promise
的實現過程,其主要使用了設計模式中的觀察者模式
- 透過
Promise.prototype.then
和Promise.prototype.catch
方法將觀察者方法註冊到被觀察者Promise
物件中,同時返回一個新的Promise
物件,以便可以鏈式呼叫 - 被觀察者管理內部
pending
、fulfilled
和rejected
的狀態轉變,同時透過建構函式中傳遞的resolve
和reject
方法以主動觸發狀態轉變和通知觀察者
三、參考
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2660713/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 淺析PromisePromise
- JavaScript之淺析PromiseJavaScriptPromise
- 淺析setTimeout與PromisePromise
- Webpack 原理淺析Web
- BTrace 原理淺析
- AQS原理淺析AQS
- koa原理淺析
- 淺析DES原理
- Seata原理淺析
- InheritedWidget原理淺析
- markdown-it 原理淺析
- TCP IP原理淺析TCP
- Webpack相關原理淺析Web
- JavaScript模組化原理淺析JavaScript
- Array、Slice、Map原理淺析
- Vuex 原理淺析筆記Vue筆記
- ArrayList底層原理淺析
- mydumper使用及原理淺析
- HashSet淺析原理學習
- WebSocket 實現原理淺析Web
- MySQL事務原理淺析MySql
- hashmap實現原理淺析HashMap
- 淺析RunLoop原理及其應用OOP
- Flutter 高效能原理淺析Flutter
- vue.js框架原理淺析Vue.js框架
- react-loadable原理淺析React
- Flutter動畫實現原理淺析Flutter動畫
- webpack系列--淺析webpack的原理Web
- 淺析Hadoop基礎原理Hadoop
- 淺析volatile原理及其使用
- redux-saga 原理淺析Redux
- 淺析Java8 Stream原理Java
- Zookeeper ZAB協議原理淺析協議
- 淺析瀑布流佈局原理
- iOS應⽤簽名原理淺析iOS
- Inkpad繪圖原理淺析繪圖
- Netty 實現原理淺析Netty
- 淺析Vite本地構建原理Vite