在寫javascript時我們往往離不開異步操作,過去我們往往通過回調函數多層嵌套來解決后一個異步操作依賴前一個異步操作,然后為了解決回調地域的痛點,出現了一些解決方案比如事件訂閱/發布的、事件監聽的方式,再后來出現了Promise、Generator、async/await等的異步解決方案。co模塊使用了Promise自動執行Generator,async/await這個Node7.6開始默認支持的最新解決方案也是依賴於Promise,所以了解Promise是非常有必要的,而理解它背后的實現原理則能在使用它的時候更加游刃有余。
實現一個簡單的異步方案
我們知道Promise
實現多個相互依賴異步操作的執行是通過.then
來實現的,我們會不由發出疑問,后面的操作是如何得知前面異步操作的完成的,我們可能會產生一種想法,后面有一個函數在一直監聽着前面異步操作的完成,你說的是發布/訂閱模式?Promise的實現個人覺得也有點發布/訂閱的味道,不過它因為有.then
的鏈式調用,又沒有使用on/emit這種很明顯的訂閱/發布的東西,讓實現變得看起來有點復雜
不過我們可以先想想發布/訂閱是怎么做的,首先有一個事件數組來收集事件,然后訂閱通過on將事件放入數組,emit觸發數組相應事件,嗯嗯,這並不是很復雜,理解了這個以后,我們開始真正地講解實現。
Promise其實內部也有一個defers
隊列存放事件,.then
的事件就在里面,聰明的你就想到了,程序開始執行的時候,.then
就已經放入下一個事件,然后后面當異步操作完成時,resolve
觸發事件隊列中的事件,便完成了一個.then
操作, 其實到這里我們就可以很快地想出一種解決方案,每次異步操作完成通過resolve
觸發事件並將事件從事件隊列中移除,通過事件隊列中的事件的resolve
使事件的觸發持續下去,我們可以用十幾行代碼就可以實現這樣的邏輯,實現一個簡單的異步編程方案
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
|
function P(fn) {
var value = null;
var events = [];
this.then = function(f) {
events.push(f);
return this;
}
function resolve(newValue) {
var f = events.shift();
f(newValue, resolve);
}
fn(resolve);
}
function a() {
return new P(function(resolve) {
console.log("get...");
setTimeout(
function() {
console.log("get 1");
resolve(
1);
},
1000)
});
}
a().then(
function(value, resolve) {
console.log("get...");
setTimeout(
function() {
console.log("get 2");
resolve(
2);
},
1000)
}).then(
function(value, resolve) {
console.log(value)
})
|
這樣就得到控制台如下的結果
1
2
3
4
5
|
get...
get 1
get...
get 2
2
|
我們當然只是初步地簡單接觸異步的一種方案,我們沒有reject
,沒有進行錯誤處理,這不是完整的,讀者想要擴展的話,可以再自行去實現,接下來我們要去接觸真正的 Promises/A+規范所實現的Promise
簡單理解Promise/A+規范的promise背后的實現
Promise/A+規范: https://promisesaplus.com/
我是通過這篇《剖析 Promise 之基礎篇》學習的,本文后面使用的代碼也是來自於此文,讀者可以先看完上文再來加深理解。
假設我們有一個場景,我們需要異步先獲取到用戶id,再通過用戶id異步再獲取到用戶名字,拿到名字輸出,
我們很迅速地寫出Promise的代碼(因為不是Promise的完整實現,就用MyPromise)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function getID() {
return new MyPromise(function(resolve, reject) {
console.log("get id...");
setTimeout(
function() {
resolve(
"666");
},
1000);
})
}
function getNameByID(id) {
return new MyPromise(function(resolve, reject) {
console.log(id);
console.log("get name...");
setTimeout(
function() {
resolve(
"hjm");
},
1000);
})
}
getID().then(getNameByID).then(
function(name) {
console.log(name);
},
function(err) {
console.log(err);
});
|
正確輸出了我們想要的結果,后面的fn拿到了前面resolve的value
1
2
3
4
|
get id...
666
get name...
hjm
|
其實我們最大的疑問會在於兩個promise它是如何通過.then
連接起來的,一圖勝千言。
橙色:是剛開始初始化產生的東西(一堆.then產生的)
紫色:是異步開始執行后的一系列流程
第一眼看起來很復雜,下面我們慢慢去一步步拆開
先拋開紫色的不看
每個Promise實例包含狀態state
、事件隊列defers
、value
、resolve
、reject
還有一個handle
函數,當狀態為pending
時是將Defered{}(包含onFulfilled、onRejected、resolve、reject)放入隊列的操作,當狀態為fulfilled
或rejected
會執行相應事件的函數onFulfilled
/onRejected
並且resolve返回的東西
然后為了實現串行Promise,.then
其實又產生了一個新的Promise實例作為中間Promise,
它將then
里的函數再與自己的實例中的resolve
,reject
共同組成一個Defered{}(包含onFulfilled、onRejected、resolve、reject),注意這里非常關鍵,它放入了自己實例的resolve
、reject
,這將是串行Promise橋梁的關鍵之處(通過閉包實現的),用handle
函數把這個對象放入前一個Promise實例的事件隊列里
異步開始!
紫色:是異步開始執行后的一系列流程
跟着標號看~假如前面的東西理解的話,你會看得下去的~哈哈
- getID setTimeout 1000s時間到,調用實例的resolve(
“666”) - 當前Promise實例的狀態改變(等待=>完成),實例的value(=>666)
- 調用當前handle函數,由於狀態是fulfilled,傳入當前value 666進入事件隊列中的相應函數(它返回的也是一個Promise),getNameByID開始執行
- 調用resolve通過判斷返回的是不是Promise,如果是的話就調用當前返回的.then
- 調用.then將前面實例的resolve、reject傳過去作為onFulfilled、onRejected
- 可以仔細看圖的這條線,這樣就很奇妙地將這個事件隊列中返回的promise和下一個.then中間Promise串起來了,它們引用都是同樣的resolve、reject
- 當第二個異步操作getNameByID setTimeout 1000s再次執行完成,調用實例的resolve(“hjm”)
- 當前Promise實例的狀態改變(等待=>完成),實例的value(=>hjm)
- 調用當前handle函數,由於狀態是fulfilled,傳入當前value hjm進入事件隊列中的相應函數,其實就是下一個中間Promise的resolve(“hjm”)
- 當前中間Promise實例的狀態改變(等待=>完成),實例的value(=>hjm)
- 調用當前handle函數,由於狀態是fulfilled,傳入當前value hjm進入事件隊列中的相應函數,打印出console.log(“hjm”),成功拿到name
- 調用resolve,發現事件隊列已經沒有東西了,程序也就結束了
此文的代碼地址在github上:https://github.com/BUPT-HJM/study-js/blob/master/%E5%85%B6%E4%BB%96/promise.js
想要自己運行的同學可以試試看,理清了整個流程會對Promise清晰很多~
Promise的小test
這兩個問題是從餓了么 node-interview摘出
判斷輸出以及相應的時間
1
2
3
4
5
6
7
8
9
10
|
let doSth = new Promise((resolve, reject) => {
console.log('hello');
resolve();
});
setTimeout(
() => {
doSth.then(
() => {
console.log('over');
})
},
10000);
|
判斷輸出順序
1
2
3
4
5
6
7
8
9
10
11
12
13
|
setTimeout(
function() {
console.log(1)
},
0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i ==
9999 && resolve();
}
console.log(3);
}).then(
function() {
console.log(4);
});
console.log(5);
|
解答
其實這兩題用三個tip就可以解決
- Promise函數調用就執行
- Promise/A+規范中then置於當前事件循環的末尾
- setTimeout(fn,0)會在下一個事件循環出現
這里往深處分析,涉及到event loop、macro-task、micro-task等一些東西,個人也沒怎么深入了解,就不加以深入分析了
有興趣的同學可以閱讀: https://github.com/creeperyang/blog/issues/21
回到題目,第一題由tip1,所以是馬上console.log(hello),然后隔10s后輸出over
第二題用用三個tip,Promise執行輸出2,調用resolve,再輸出3,然后調用then將輸出4置於事件循環末尾,然后輸出5,到達末尾,輸出4,下一個事件循環,輸出剛開始的1,所以順序是23541