承上啟下
基礎預熱:你好,JavaScript非同步程式設計---- 理解JavaScript非同步的美妙
理解非同步之美:Promise與async await(一)
經歷了上一篇基礎的Promise講解後我覺得大家對於promise的基本用法和想法就有一定了解了。(就是一種承諾喲)
下面我們要去了解一下它的工作流程
結合原始碼與分析別人的常見實現進行理解
下面是別人實現的總原始碼,(簡單一看就可以)
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
function Promise(callback) {
this.status = PENDING;
this.value = null;
this.defferd = [];
setTimeout(callback.bind(this, this.resolve.bind(this), this.reject.bind(this)), 0);
}
Promise.prototype = {
constructor: Promise,
resolve: function (result) {
this.status = FULFILLED;
this.value = result;
this.done();
},
reject: function (error) {
this.status = REJECTED;
this.value = error;
},
handle: function (fn) {
if (!fn) {
return;
}
var value = this.value;
var t = this.status;
var p;
if (t == PENDING) {
this.defferd.push(fn);
} else {
if (t == FULFILLED && typeof fn.onfulfiled == 'function') {
p = fn.onfulfiled(value);
}
if (t == REJECTED && typeof fn.onrejected == 'function') {
p = fn.onrejected(value);
}
var promise = fn.promise;
if (promise) {
if (p && p.constructor == Promise) {
p.defferd = promise.defferd;
} else {
p = this;
p.defferd = promise.defferd;
this.done();
}
}
}
},
done: function () {
var status = this.status;
if (status == PENDING) {
return;
}
var defferd = this.defferd;
for (var i = 0; i < defferd.length; i++) {
this.handle(defferd[i]);
}
},
then: function (success, fail) {
var o = {
onfulfiled: success,
onrejected: fail
};
var status = this.status;
o.promise = new this.constructor(function () {});
if (status == PENDING) {
this.defferd.push(o);
} else if (status == FULFILLED || status == REJECTED) {
this.handle(o);
}
return o.promise;
}
};
複製程式碼
這是網上一份常見的Promise的原始碼實現我會對這個進行一個分析 (肯定有人問為什麼不自己實現一個? 解:省時、網上太多了、本質還是要了解思想)
話不多說開始咯
我們們先大體梳理一下實現的東西要能幹什麼?
first :
let promsie = new Promise((resolve,reject)=>{
doSomething()
})
根據這個建構函式,我們需要實現兩個方法,resolve、reject方法。
複製程式碼
second :
promise.then(res=>{
doSomethingRes();
},rej=>{
doSomenthingRej()
})
我們要實現一個then方法,可以去根據不同狀態來執行不同的函式。
複製程式碼
最基本的兩個內容我們已經確定了。
話不多說開始分析程式碼。
對程式碼的分析在內容的註釋上大家不要遺漏哈!!!
第一段建構函式與狀態設定
// 首先宣告三個狀態,
// 狀態的就是上一節說的:事情是進行中、已完成、失敗了。
// 這三種狀態遵循著PENDING->FULEFILLED 或者PENDING->REJECTED
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
// Promise建構函式接收u一個回撥函式
function Promise(callback) {
// 新new出來的例項的status一定是PENDING.
this.status = PENDING;
// value是指當你事情完成、失敗後內部儲存的值
// 用法是resolve(42) 在then函式的res=>{dosomething()res就是42
}
this.value = null;
// defferd 字面意思推遲、是個陣列,存放這個promise以後要執行的事件
// 類比釋出訂閱模式(觀察者模式)。存放觀察者在被觀察者身上訂閱的事件列表
// defferd內的事件存放著日後觀察者要執行的事件。
this.defferd = [];
// setTimeout非同步的去執行new 一個promise例項內執行的任務,不去阻塞主執行緒。
//這一段程式碼我有一點疑惑,new promise例項時,callback的執行並不是非同步的。
// 而這裡選擇非同步的並不是很合理。
// bind函式的作用。callback的引數如何指定?通過bind方法將函式函式柯里化(Currying 和一個NBA球星的名字一樣很好記)
// 而且繫結函式的this執行。函式內的this都執行這個new 出來的promise例項
// 去指定函式執行時的引數,將resolve方法與reject方法強制做為callback的引數。
// 所以我們寫的回撥函式,引數怎麼命名都可以執行到對應的resolve與reject方法
setTimeout(callback.bind(this, this.resolve.bind(this), this.reject.bind(this)), 0);
}
複製程式碼
自己實現與官方Promise執行的對比。大家可以看一下這個setTimeout導致的執行順序問題。所以閱讀別人對各種功能實現時要學會對照的去看。
到這裡夥伴們已經瞭解了我們new 一個建構函式時都會做哪些事情。
1:對promise例項定義一個狀態,值為PENDING。
2:給promise例項定義一個存放值的空間。
3:設定一個釋出列表,在以後的指定時間釋出其中的事件。
4:通過bind函式將callback柯里化,使callback執行時呼叫對應的resolve與reject方法,並執行callback
第二段 resolve reject then方法的分析
為什麼先說這三個方法。 因為resolve、reject是核心方法,不說都不行,可是resolve與reject完成要做的事情必須是then方法指定的所以三個方法之間關係密切。
// 在Promise的原型物件上指定這些方法。
// 這種做法有很大弊端、並且Promise原始碼也並不是這麼做的之後會進行分析
Promise.prototype = {
// 覆蓋式的指定Promise的原型物件會導致constructor屬性丟失
// 在這裡進行填補,手動指定Promise原型物件上的constructor屬性
constructor: Promise,
// resolve方法開始 接收一個結果(可以為空)
resolve: function (result) {
//更改狀態為FULFILLED。
this.status = FULFILLED;
// 將result存放在之前建構函式中提到的存放結果的空間中
this.value = result;
// done方法。表示執行完畢(後面會繼續將)
this.done();
},
// 與resolve方法類似 不多做解釋
reject: function (error) {
// 狀態更改
this.status = REJECTED;
this.value = error;
// 沒有done函式,這塊做法很有問題下面會配圖解釋。
},
// then方法開始要好好講講
// success表示狀態變成FULFILLED時要執行的函式
// fail表示狀態變成REJECTED時要執行的函式
then: function (success, fail) {
// 宣告一個物件來存放這些事件。
var o = {
onfulfiled: success,
onrejected: fail
};
// 獲取當前promise的狀態。
// 這個的意義是,我們對promise例項執行then方法的時候有兩種情況
// 一:promise例項的內容還沒有執行完畢。二:promise例項內容已經執行完畢並且狀態已經改變。繼續下面。
var status = this.status;
o.promise = new this.constructor(function () {});
// 如果狀態是PENDING,表示例項內容還未執行完畢。
// 說明then方法指定在某種狀態要執行的事件是未來發生的現在並不執行。
// 所以將o放入defferd訂閱列表中。
// 這個之前講過defferd中的內容會在某個條件觸發後會執行。
// 所以當promise例項內容還未完成就要把未來執行的方法放入訂閱列表
if (status == PENDING) {
this.defferd.push(o);
// 對應之前說的情況二
// 狀態變成以下兩種情況怎麼辦?
// 訂閱列表內不應該有當前情況的o。
// 所以要立即執行當前指定的事件,而且未來的任何情況下這次指定的事件也不會執行。
//所以不會放入defferd中
} else if (status == FULFILLED || status == REJECTED) {
// 執行handle函式
this.handle(o);
}
// then方法存在鏈式呼叫,then方法的返回值必須是一個promise物件
return o.promise;
}
};
複製程式碼
到這裡我們大體梳理清楚,resolve、reject、then方法的用處。
提一嘴 reject方法沒有執行done函式會導致以下情況
一:在new promise例項過程中執行的callback函式,在函式執行的過程中肯定會呼叫resolve或者reject(兩個都呼叫也可能)。當呼叫了resolve方法之後會改變promise的狀態,存放結果。表示任務完成,執行done函式。(reject就不再來一遍了)
二:then方法的執行事件與resolve方法沒有任何先後順序可言。隨心所欲誰在前面都不一定。在resolve(reject)之前執行,就註冊一下要執行的事件。在resolve(reject)之後執行就直接執行就可以了,並且不要註冊。
第三段 具體是怎麼執行的呢?聊聊done與handle
同學們按照程式碼執行的順序,我們應該先去看done方法。然後再看handle所以辛苦一下,先向下一點找到可愛的done方法。
// 看完done方法的朋友肯定對我這種方式很鬧心,保證你們看的熱情嘛。哈哈哈哈哈
// 沒看done方法的快回去看、快回去看。
// 必須提一下 下面一直說的o是什麼? o是在then方法中傳入defferd陣列中的物件,一下簡稱為o。
// handle是幹嘛的??? 那是用來執行o的。o裡面放著我們想要執行的內容
// 大家再回憶以下,handle還在哪裡執行了?想起來了吧,當then方法執行時
// 如果狀態已經改變了。那麼就直接handle(o),執行你要做的事情。
handle: function (fn) {
// o不存在的???媽耶,咋辦呀。那就是defferd中沒東西。好吧什麼都不做
if (!fn) {
return;
}
var value = this.value;
var t = this.status;
var p;
// 如果狀態為PENDING,表示還沒到o要執行的內容。那麼不能執行的?
if (t == PENDING) {
this.defferd.push(fn);
} else {
// 這裡面很容易看的,狀態變成FULFILLED,
//並且你在給狀態是FULFILLED時要做的事情可以執行(函式才能執行呀,你寫個字串不報錯了??)
// 執行咯。這裡面大家一看就知道,你then方法裡面res=>{doSometthing(res)}
//這個res就是promise記憶體放的結果(value)。
if (t == FULFILLED && typeof fn.onfulfiled == 'function') {
p = fn.onfulfiled(value);
}
// 不多提了。
if (t == REJECTED && typeof fn.onrejected == 'function') {
p = fn.onrejected(value);
}
// 但是這個p是幹什麼的????
// 存放方法的返回值,為了鏈式呼叫。實現鏈式呼叫的是什麼方法?
// 返回的o.promise. 那麼返回的o.promise不存在呢?(當然這是不可能的)
// 那就沒有鏈式呼叫。
// promise中有一個方法,當then函式內的事件(指的這個函式:res=>{})
// 返回值是一個promise物件時,那麼then的返回值就是這個promise物件
// 鏈式中的下一個then就會等待這個promise執行完畢。
// 如果不是promise物件怎麼辦?那麼就執行鏈條後面的then方法註冊的事情。
var promise = fn.promise;
if (promise) {
// 如果你註冊的事件執行後的返回值是一個promise物件
if (p && p.constructor == Promise) {
// 當前這個p(是個promise物件)可以把o.promise物件訂閱列表內的事件拿過來。
// 序列後,返回的promise繼續控制著defferd的內容。
//理論上講,按剛才的邏輯來寫,每個promise物件內的defferd,都應該只有一個值。
// 因為序列的鏈條每個then註冊的事件都在上一個then返回的o.promise的defferd內。
// 那麼為什麼?defferd要寫個陣列呢???這是我疑惑的地方但是影響不大
p.defferd = promise.defferd;
} else {
// 如果不是promise呢?那麼就把當前的promise物件當作你的返回值
// 繼續繼承o.promise裡面的defferd
p = this;
p.defferd = promise.defferd;
// 並沒有任何承諾,p應該就是一個resolved(reject)狀態
// 直接執行done方法就可以啦。
this.done();
}
}
}
},
// done方法是在resolve方法(reject為啥沒寫之前有講過)中執行的,表示resolve方法執行完畢了。
// 這個完畢是一種明確訊號,那就是之前說好的狀態變成FULFILLED我要做的那些事情。
// 鄉親們、弟兄們我們們可以被執行了
done: function () {
// 當然為了保險起見PENDING狀態肯定不會執行,return掉。
// done函式在PENDING狀態也不會被呼叫呀。雙重保險嘛
var status = this.status;
if (status == PENDING) {
return;
}
// 鄉親們、兄弟們在哪呢?就在你的defferd中。我們的訂閱列表內的兄弟們該執行了。
// 遍歷以下我們的defferd物件,誰也別漏下都給我執行了。
var defferd = this.defferd;
for (var i = 0; i < defferd.length; i++) {
// 問題來了鄉親們不是函式是一個物件啊。
//為啥是物件?在then方法中有defferd.push(o);o是啥?是個物件啊。
// 那咋執行????需要一個特定來執行o的方法,就是handle。
// 好了夥伴們可以把文章回到上面一點了。
this.handle(defferd[i]);
}
},
複製程式碼
這一段我已經把handle與done方法說完了。主要是為了鏈式呼叫。才會設計的這樣子。所以鏈式呼叫還是很搶手的一個功能。 自己也可以嘗試的去實現一下符合promise規範的promise功能。
親! 學習完要思考
不知道看到這裡大家對網上常見的promise原始碼實現有一種什麼樣的感覺???
我先說說我的感覺
看過原始碼(抱歉我的智商是在有限,短時間內是真的看不懂啊),覺得原始碼做的要合理太多了。看不懂我都覺得合理。。。。不是對強者的過分崇拜,而是真的很合理。網上常見的實現,只是單單的實現了功能,這樣的promise只適合有一定promise經驗並且守規矩的人使用。為什麼這麼說???
一:這樣實現的promise,狀態可以隨時人為的更改,對外暴露,沒有設定為私有屬性。
二:為了方便,選擇把方法設定在原型鏈上,導致無法使用私用變數。
三:reject的執行不足,只是對resolve進行合理的使用。
雖然我這麼說,我也實現不出來,寫出這些的人還是比我厲害很多
promise的原始碼則是(某個版本的,版本號我不記得了)
把resolve、reject、all、race,handle方法,都放在建構函式內。
把catch、then、chain方法放在原型上。
有圖為證,字面意思應該是這個意思,我覺得我沒想錯。 在改變promise的狀態也好、value也好。都在頻繁的使用PromiseSet方法來設定屬性,對方法進行封裝,並且方便狀態的管理,附加合理的容錯。對比原始碼之後,覺得自己雖然流程大體瞭解,但是這種精密而且優雅的方式,是短時間內很難去掌握的。promise的原始碼當然會堅持看下去,網上能把promise按照規範實現一遍的人已經很厲害了。我雖然覺得還有地方可以修改,但是我比他們還差的遠(這種感覺就有點像:我不上,我就比比),要向他們學習,照這他們去努力。
別說了 喝雞湯吧
前端的學習之路還很漫長,我看過的(僅僅是看過的)原始碼半隻手就都數的過來。還是堅信堅持下去,自己就變得很棒。每個人都是從控制流語句學過來的,邏輯也不過是複雜的控制流程(還涉及高階的演算法與設計模式),堅信自己一定可以成功!!!一起努力吧 每一個前端er(boy and girl)。所以一切原始碼層面看不懂、不理解都可以歸結為看得少、想得少、理解的少。(和你的智商沒有任何關係喲)
下期預告
下一篇就是理解非同步之美的終點篇了。非同步的美好在於這種神奇的思想。抓住思想的尾巴,不被技術束縛,嘿嘿嘿。
要開新課題了。課題應該是圍繞著vue-router的原始碼進行學習。一個與大家分享學習過程的週期性文章。盡情期待!!!