30分鐘,讓你徹底明白Promise原理
前言
前一陣子記錄了promise的一些常規(guī)用法,這篇文章再深入一個層次,來分析分析promise的這種規(guī)則機(jī)制是如何實現(xiàn)的。ps:本文適合已經(jīng)對promise的用法有所了解的人閱讀,如果對其用法還不是太了解,可以移步我的上一篇博文。
本文的promise源碼是按照Promise/A+規(guī)范來編寫的(不想看英文版的移步Promise/A+規(guī)范中文翻譯)
引子
為了讓大家更容易理解,我們從一個場景開始講解,讓大家一步一步跟著思路思考,相信你一定會更容易看懂。
考慮下面一種獲取用戶id的請求處理
- //例1
- function getUserId() {
- return new Promise(function(resolve) {
- //異步請求
- http.get(url, function(results) {
- resolve(results.id)
- })
- })
- }
- getUserId().then(function(id) {
- //一些處理
- })
getUserId方法返回一個promise,可以通過它的then方法注冊(注意注冊這個詞)在promise異步操作成功時執(zhí)行的回調(diào)。這種執(zhí)行方式,使得異步調(diào)用變得十分順手。
原理剖析
那么類似這種功能的Promise怎么實現(xiàn)呢?其實按照上面一句話,實現(xiàn)一個最基礎(chǔ)的雛形還是很easy的。
極簡promise雛形
- function Promise(fn) {
- var value = null,
- callbacks = []; //callbacks為數(shù)組,因為可能同時有很多個回調(diào)
- this.then = function (onFulfilled) {
- callbacks.push(onFulfilled);
- };
- function resolve(value) {
- callbacks.forEach(function (callback) {
- callback(value);
- });
- }
- fn(resolve);
- }
上述代碼很簡單,大致的邏輯是這樣的:
- 調(diào)用then方法,將想要在Promise異步操作成功時執(zhí)行的回調(diào)放入callbacks隊列,其實也就是注冊回調(diào)函數(shù),可以向觀察者模式方向思考;
- 創(chuàng)建Promise實例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù),即resolve,它接收一個參數(shù)value,代表異步操作返回的結(jié)果,當(dāng)一步操作執(zhí)行成功后,用戶會調(diào)用resolve方法,這時候其實真正執(zhí)行的操作是將callbacks隊列中的回調(diào)一一執(zhí)行;
可以結(jié)合例1中的代碼來看,首先new Promise時,傳給promise的函數(shù)發(fā)送異步請求,接著調(diào)用promise對象的then屬性,注冊請求成功的回調(diào)函數(shù),然后當(dāng)異步請求發(fā)送成功時,調(diào)用resolve(results.id)方法, 該方法執(zhí)行then方法注冊的回調(diào)數(shù)組。
相信仔細(xì)的人應(yīng)該可以看出來,then方法應(yīng)該能夠鏈?zhǔn)秸{(diào)用,但是上面的最基礎(chǔ)簡單的版本顯然無法支持鏈?zhǔn)秸{(diào)用。想讓then方法支持鏈?zhǔn)秸{(diào)用,其實也是很簡單的:
- this.then = function (onFulfilled) {
- callbacks.push(onFulfilled);
- return this;
- };
see?只要簡單一句話就可以實現(xiàn)類似下面的鏈?zhǔn)秸{(diào)用:
- // 例2
- getUserId().then(function (id) {
- // 一些處理
- }).then(function (id) {
- // 一些處理
- });
加入延時機(jī)制
細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn),上述代碼可能還存在一個問題:如果在then方法注冊回調(diào)之前,resolve函數(shù)就執(zhí)行了,怎么辦?比如promise內(nèi)部的函數(shù)是同步函數(shù):
- // 例3
- function getUserId() {
- return new Promise(function (resolve) {
- resolve(9876);
- });
- }
- getUserId().then(function (id) {
- // 一些處理
- });
這顯然是不允許的,Promises/A+規(guī)范明確要求回調(diào)需要通過異步方式執(zhí)行,用以保證一致可靠的執(zhí)行順序。因此我們要加入一些處理,保證在resolve執(zhí)行之前,then方法已經(jīng)注冊完所有的回調(diào)。我們可以這樣改造下resolve函數(shù):
- function resolve(value) {
- setTimeout(function() {
- callbacks.forEach(function (callback) {
- callback(value);
- });
- }, 0)
- }
上述代碼的思路也很簡單,就是通過setTimeout機(jī)制,將resolve中執(zhí)行回調(diào)的邏輯放置到JS任務(wù)隊列末尾,以保證在resolve執(zhí)行時,then方法的回調(diào)函數(shù)已經(jīng)注冊完成.
但是,這樣好像還存在一個問題,可以細(xì)想一下:如果Promise異步操作已經(jīng)成功,這時,在異步操作成功之前注冊的回調(diào)都會執(zhí)行,但是在Promise異步操作成功這之后調(diào)用的then注冊的回調(diào)就再也不會執(zhí)行了,這顯然不是我們想要的。
加入狀態(tài)
恩,為了解決上一節(jié)拋出的問題,我們必須加入狀態(tài)機(jī)制,也就是大家熟知的pending、fulfilled、rejected。
Promises/A+規(guī)范中的2.1Promise States中明確規(guī)定了,pending可以轉(zhuǎn)化為fulfilled或rejected并且只能轉(zhuǎn)化一次,也就是說如果pending轉(zhuǎn)化到fulfilled狀態(tài),那么就不能再轉(zhuǎn)化到rejected。并且fulfilled和rejected狀態(tài)只能由pending轉(zhuǎn)化而來,兩者之間不能互相轉(zhuǎn)換。一圖勝千言:
改進(jìn)后的代碼是這樣的:
- function Promise(fn) {
- var state = 'pending',
- value = null,
- callbacks = [];
- this.then = function (onFulfilled) {
- if (state === 'pending') {
- callbacks.push(onFulfilled);
- return this;
- }
- onFulfilled(value);
- return this;
- };
- function resolve(newValue) {
- value = newValue;
- state = 'fulfilled';
- setTimeout(function () {
- callbacks.forEach(function (callback) {
- callback(value);
- });
- }, 0);
- }
- fn(resolve);
- }
上述代碼的思路是這樣的:resolve執(zhí)行時,會將狀態(tài)設(shè)置為fulfilled,在此之后調(diào)用then添加的新回調(diào),都會立即執(zhí)行。
這里沒有任何地方將state設(shè)為rejected,為了讓大家聚焦在核心代碼上,這個問題后面會有一小節(jié)專門加入。
鏈?zhǔn)絇romise
那么這里問題又來了,如果用戶再then函數(shù)里面注冊的仍然是一個Promise,該如何解決?比如下面的例4:
- // 例4
- getUserId()
- .then(getUserJobById)
- .then(function (job) {
- // 對job的處理
- });
- function getUserJobById(id) {
- return new Promise(function (resolve) {
- http.get(baseUrl + id, function(job) {
- resolve(job);
- });
- });
- }
這種場景相信用過promise的人都知道會有很多,那么類似這種就是所謂的鏈?zhǔn)絇romise。
鏈?zhǔn)絇romise是指在當(dāng)前promise達(dá)到fulfilled狀態(tài)后,即開始進(jìn)行下一個promise(后鄰promise)。那么我們?nèi)绾毋暯赢?dāng)前promise和后鄰promise呢?(這是這里的難點(diǎn))。
其實也不是辣么難,只要在then方法里面return一個promise就好啦。Promises/A+規(guī)范中的2.2.7就是這么說噠(微笑臉)~
下面來看看這段暗藏玄機(jī)的then方法和resolve方法改造代碼:
- function Promise(fn) {
- var state = 'pending',
- value = null,
- callbacks = [];
- this.then = function (onFulfilled) {
- return new Promise(function (resolve) {
- handle({
- onFulfilled: onFulfilled || null,
- resolve: resolve
- });
- });
- };
- function handle(callback) {
- if (state === 'pending') {
- callbacks.push(callback);
- return;
- }
- //如果then中沒有傳遞任何東西
- if(!callback.onResolved) {
- callback.resolve(value);
- return;
- }
- var ret = callback.onFulfilled(value);
- callback.resolve(ret);
- }
- function resolve(newValue) {
- if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
- var then = newValue.then;
- if (typeof then === 'function') {
- then.call(newValue, resolve);
- return;
- }
- }
- state = 'fulfilled';
- value = newValue;
- setTimeout(function () {
- callbacks.forEach(function (callback) {
- handle(callback);
- });
- }, 0);
- }
- fn(resolve);
- }
我們結(jié)合例4的代碼,分析下上面的代碼邏輯,為了方便閱讀,我把例4的代碼貼在這里:
- // 例4
- getUserId()
- .then(getUserJobById)
- .then(function (job) {
- // 對job的處理
- });
- function getUserJobById(id) {
- return new Promise(function (resolve) {
- http.get(baseUrl + id, function(job) {
- resolve(job);
- });
- });
- }
- then方法中,創(chuàng)建并返回了新的Promise實例,這是串行Promise的基礎(chǔ),并且支持鏈?zhǔn)秸{(diào)用。
- handle方法是promise內(nèi)部的方法。then方法傳入的形參onFulfilled以及創(chuàng)建新Promise實例時傳入的resolve均被push到當(dāng)前promise的callbacks隊列中,這是銜接當(dāng)前promise和后鄰promise的關(guān)鍵所在(這里一定要好好的分析下handle的作用)。
- getUserId生成的promise(簡稱getUserId promise)異步操作成功,執(zhí)行其內(nèi)部方法resolve,傳入的參數(shù)正是異步操作的結(jié)果id
- 調(diào)用handle方法處理callbacks隊列中的回調(diào):getUserJobById方法,生成新的promise(getUserJobById promise)
- 執(zhí)行之前由getUserId promise的then方法生成的新promise(稱為bridge promise)的resolve方法,傳入?yún)?shù)為getUserJobById promise。這種情況下,會將該resolve方法傳入getUserJobById promise的then方法中,并直接返回。
- 在getUserJobById promise異步操作成功時,執(zhí)行其callbacks中的回調(diào):getUserId bridge promise中的resolve方法
- ***執(zhí)行g(shù)etUserId bridge promise的后鄰promise的callbacks中的回調(diào)。
更直白的可以看下面的圖,一圖勝千言(都是根據(jù)自己的理解畫出來的,如有不對歡迎指正):
失敗處理
在異步操作失敗時,標(biāo)記其狀態(tài)為rejected,并執(zhí)行注冊的失敗回調(diào):
- //例5
- function getUserId() {
- return new Promise(function(resolve) {
- //異步請求
- http.get(url, function(error, results) {
- if (error) {
- reject(error);
- }
- resolve(results.id)
- })
- })
- }
- getUserId().then(function(id) {
- //一些處理
- }, function(error) {
- console.log(error)
- })
有了之前處理fulfilled狀態(tài)的經(jīng)驗,支持錯誤處理變得很容易,只需要在注冊回調(diào)、處理狀態(tài)變更上都要加入新的邏輯:
- function Promise(fn) {
- var state = 'pending',
- value = null,
- callbacks = [];
- this.then = function (onFulfilled, onRejected) {
- return new Promise(function (resolve, reject) {
- handle({
- onFulfilled: onFulfilled || null,
- onRejected: onRejected || null,
- resolve: resolve,
- reject: reject
- });
- });
- };
- function handle(callback) {
- if (state === 'pending') {
- callbacks.push(callback);
- return;
- }
- var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
- ret;
- if (cb === null) {
- cb = state === 'fulfilled' ? callback.resolve : callback.reject;
- cb(value);
- return;
- }
- ret = cb(value);
- callback.resolve(ret);
- }
- function resolve(newValue) {
- if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
- var then = newValue.then;
- if (typeof then === 'function') {
- then.call(newValue, resolve, reject);
- return;
- }
- }
- state = 'fulfilled';
- value = newValue;
- execute();
- }
- function reject(reason) {
- state = 'rejected';
- value = reason;
- execute();
- }
- function execute() {
- setTimeout(function () {
- callbacks.forEach(function (callback) {
- handle(callback);
- });
- }, 0);
- }
- fn(resolve, reject);
- }
上述代碼增加了新的reject方法,供異步操作失敗時調(diào)用,同時抽出了resolve和reject共用的部分,形成execute方法。
錯誤冒泡是上述代碼已經(jīng)支持,且非常實用的一個特性。在handle中發(fā)現(xiàn)沒有指定異步操作失敗的回調(diào)時,會直接將bridge promise(then函數(shù)返回的promise,后同)設(shè)為rejected狀態(tài),如此達(dá)成執(zhí)行后續(xù)失敗回調(diào)的效果。這有利于簡化串行Promise的失敗處理成本,因為一組異步操作往往會對應(yīng)一個實際功能,失敗處理方法通常是一致的:
- //例6
- getUserId()
- .then(getUserJobById)
- .then(function (job) {
- // 處理job
- }, function (error) {
- // getUserId或者getUerJobById時出現(xiàn)的錯誤
- console.log(error);
- });
異常處理
細(xì)心的同學(xué)會想到:如果在執(zhí)行成功回調(diào)、失敗回調(diào)時代碼出錯怎么辦?對于這類異常,可以使用try-catch捕獲錯誤,并將bridge promise設(shè)為rejected狀態(tài)。handle方法改造如下:
- function handle(callback) {
- if (state === 'pending') {
- callbacks.push(callback);
- return;
- }
- var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
- ret;
- if (cb === null) {
- cb = state === 'fulfilled' ? callback.resolve : callback.reject;
- cb(value);
- return;
- }
- try {
- ret = cb(value);
- callback.resolve(ret);
- } catch (e) {
- callback.reject(e);
- }
- }
如果在異步操作中,多次執(zhí)行resolve或者reject會重復(fù)處理后續(xù)回調(diào),可以通過內(nèi)置一個標(biāo)志位解決。
總結(jié)
剛開始看promise源碼的時候總不能很好的理解then和resolve函數(shù)的運(yùn)行機(jī)理,但是如果你靜下心來,反過來根據(jù)執(zhí)行promise時的邏輯來推演,就不難理解了。這里一定要注意的點(diǎn)是:promise里面的then函數(shù)僅僅是注冊了后續(xù)需要執(zhí)行的代碼,真正的執(zhí)行是在resolve方法里面執(zhí)行的,理清了這層,再來分析源碼會省力的多。
現(xiàn)在回顧下Promise的實現(xiàn)過程,其主要使用了設(shè)計模式中的觀察者模式:
- 通過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法注冊到被觀察者Promise對象中,同時返回一個新的Promise對象,以便可以鏈?zhǔn)秸{(diào)用。
- 被觀察者管理內(nèi)部pending、fulfilled和rejected的狀態(tài)轉(zhuǎn)變,同時通過構(gòu)造函數(shù)中傳遞的resolve和reject方法以主動觸發(fā)狀態(tài)轉(zhuǎn)變和通知觀察者。
參考文獻(xiàn)
- 深入理解 Promise
- JavaScript Promises … In Wicked Detail