其實Promise
本身並不具備非同步
的能力,而之所以這裡需要單獨開一篇說明其原理,是因為Promise
在非同步程式設計
的過程中是一個不可或缺的一環。原因下面細說。
在說promise之前,有必要先說下JS中的回撥方式。比如下面:
function doSomethingAfterTime(time, something) {
setTimeout(fun, time);
}
複製程式碼
但是這樣的回撥方式有一個問題,可讀性太差。另外當回撥的層次多了以後,容易陷入回撥地獄。舉個例子:
function func1(cb){
// do something
cb();
}
function func2(cb){
// do something
cb();
}
function func3(cb){
// do something
cb();
}
// do
func1(function(){
func2(function(){
func3(function(){
});
});
});
複製程式碼
這樣的程式碼讀起來簡直就是折磨,暈死!
下面試著改進下程式碼,試著將回撥函式封裝起來。順便剖析下promise的原理。
Promise的原理
先來一個最簡單的。
function Promise(something){
var callback = null;
this.then = function(onCompelete){
callback = onCompelete;
};
something(function (value){
callback(value);
});
}
複製程式碼
下面是呼叫程式碼。
// 事件1
function func1(){
return new Promise(function(resolve){
// do something
setTimeout(function(){
console.log('func1');
resolve();
},1000);
});
}
func1().then(function(){
console.log('func1 complete');
});
複製程式碼
上面對Promise
的封裝算是最簡單的版本,只是模擬了Promise
的呼叫方法,比如then
,還有Promise的建構函式
。但是這樣的封裝無法實現鏈式呼叫,鏈式呼叫
的核心就是當呼叫某個方法的時候返回該物件本身
或者該物件對應class的全新物件
。而對於Promise
的改造也很簡單.
then
方法返回Promise
本身Promoise
需要支援多個callback
。
function Promise(something){
var callbacks = [];
this.then = function(onCompelete){
callbacks.push(onCompelete);
return this;
};
function resolve(value){
callbacks.forEach(function(cb){
cb(value);
});
}
something(resolve);
}
複製程式碼
呼叫程式碼如下:
func1().then(function(){
console.log('func1 complete');
}).then(function(){
console.log('then2');
}).then(function(){
console.log('then3');
});
複製程式碼
現在的Promise
執行上面的程式碼後能夠得到正確的執行結果,但是有一個問題,如果我們想在then
方法再呼叫一個返回promise
的方法?比如這樣:
// 事件2
function func2(){
return new Promise(function(resolve){
// do something
setTimeout(function(){
console.log('func2');
resolve();
},1000);
});
}
func1().then(func2).then(function(){
console.log('all complete');
});
複製程式碼
輸出如下:
func1
all complete
func2
複製程式碼
你會發現雖然func2
成功呼叫了,但是輸出順序亂了,我們期望的正確輸出順序應該是:
func1
func2
all complete
複製程式碼
分析下問題出在哪裡?問題就出在Promise
中的callbacks
,第一個then
是在func1
返回的Promise
上呼叫的,而第二個then
事實上還是在func1
返回的Promise
上呼叫的。然而我們希望的是,第二個then
應該是在func2
返回的Promise
呼叫,這時候就需要考慮如何進一步改造Promise
了。
對於then
傳入的onCompelete
函式引數,它是不知道這個函式具體是否會返回Promise
,只有呼叫了onCompelete
方法才能知道具體返回的資料。但是onCompelete
是回撥函式,你無法直接在then
中呼叫。因此需要考慮其他的方式。
如果then
方法裡面返回一個新的Promise
物件呢?用這個新的Promise
作為中間代理,比如這樣:
function Promise(something){
var callbacks = [];
this.then = function(onCompelete){
return new Promise(function (resolve) {
callbacks.push({
onCompelete: onCompelete,
resolve: resolve
});
});
};
function resolve(value){
callbacks.forEach(function(cb){
var ret = cb.onCompelete(value);
cb.resolve(ret);
})
}
something(resolve);
}
複製程式碼
但是執行的時候你會發現輸出順序還是沒變,還是有問題的。那麼繼續分析問題出在哪裡?
通過除錯發現,resolve
傳入的value
有可能是promise
物件,而我們已經在then
方法裡面返回了新的promise物件了
,交由該物件作為代理了。因此resolve
傳入的value
如果是promise
物件的話,那麼就需要把當前promise
的resolve
處理權交出去,交給傳入的promise
物件。相當於代理人
把權力交還給實際應該處理的物件。可能有點繞,我再詳細的描述下
func1
返回的promise
為p1
,then
返回的promise
為p2
,resolve
傳入的promise
物件為p3
,func2
返回的promise
物件為p4
。
上面一共提到4個
promise
物件。
說下描說下呼叫順序。
首先由func1
建立p1
,然後呼叫then
方法建立了p2
,然後再次呼叫了then
方法,由p2
建立了p3
。p2
和p3
都是由then
建立的代理人。
這時候func1中的非同步程式碼執行了,1秒過後由func1
呼叫了p1
的resolve
方法,並且將callbacks
陣列內的方法依次呼叫,然後由cb.onCompelete(value)
方法間接得到func2
返回的p4
,接著呼叫p2
的resolve
方法將p4
傳入。但是上面說了,p2
只是個代理,應該把權力
交還給p4
來執行。這樣p4
得到權力--回撥函式
,當func2
的非同步程式碼執行完畢後,由p4
來執行回撥函式。
因此resolve
方法需要進行如下改造。
function resolve(value) {
// 交還權力,並且把resolve傳過去
if (value && (typeof value.then === 'function')) {
value.then.call(value, resolve);
return;
}
callbacks.forEach(function (cb) {
var ret = cb.onCompelete(value);
cb.resolve(ret);
});
}
複製程式碼
上面的程式碼就是交權
的程式碼。這樣完全的Promise
修改如下:
function Promise(something) {
var callbacks = [];
this.then = function (onCompelete) {
return new Promise(function (resolve) {
callbacks.push({
onCompelete: onCompelete,
resolve: resolve
});
});
};
function resolve(value) {
if (value && (typeof value.then === 'function')) {
value.then.call(value, resolve);
return;
}
callbacks.forEach(function (cb) {
var ret = cb.onCompelete(value);
cb.resolve(ret);
});
}
something(resolve);
}
複製程式碼
這樣修改過後,再執行如下程式碼:
func1().then(func2).then(function () {
console.log('all complete');
});
複製程式碼
現在就能得到正確的執行結果了。
至此,一個簡單的Promise
定義完了。這時候有一個問題,如果呼叫then
方法之前resolve
已經被執行了怎麼辦呢,豈不是永遠都得不到回撥了?比如這樣:
(new Promise(function (resolve) {
resolve();
})).then(function(){
console.log('complete');
});
複製程式碼
你會發現then
裡面的回撥就不會執行了。其實這時候只需要做一個小小的改動就行了。改造如下:
function Promise(something) {
var callbacks = [];
this.then = function (onCompelete) {
return new Promise(function (resolve) {
callbacks.push({
onCompelete: onCompelete,
resolve: resolve
});
});
};
function resolve(value) {
if (value && (typeof value.then === 'function')) {
value.then.call(value, resolve);
return;
}
setTimeout(function(){
callbacks.forEach(function (cb) {
var ret = cb.onCompelete(value);
cb.resolve(ret);
});
},0);
}
something(resolve);
}
複製程式碼
你會發現,這裡只是在resolve
方法裡面,將執行的回撥放入setTimeout
中,並且timeout
設為0
。這裡稍微說下原理
在第一篇中提到
setTimeout
類似定時器
,JS內容在執行setTimeout
的回撥函式的時候使用執行緒排程
的方式將回撥函式排程到JS執行緒
執行。但凡涉及到執行緒排程
那麼肯定需要等待JS執行緒空閒的時候才能排程過來。這時候將timeout設為0,相當於改變了程式碼執行順序。
在實際的開發過程中,上面的Promise
程式碼還是缺少了一個功能,那就是狀態管理
,比如:pending
、fulfilled
、rejected
。下面的程式碼繼續加入狀態管理
的程式碼,先新增pending
和fulfilled
的狀態:
function Promise(something) {
var callbacks = [];
var state = 0;//0:pending,1:fulfilled
var resultValue = null;
this.then = function (onCompelete) {
return new Promise(function (resolve) {
handleCallBack({
onCompelete: onCompelete,
resolve: resolve
});
});
};
function handleCallBack(callback){
switch(state){
case 0:{
callbacks.push(callback);
break;
}
case 1:{
var ret = callback.onCompelete(resultValue);
callback.resolve(ret);
break;
}
default:{
break;
}
}
}
function resolve(value) {
if (value && (typeof value.then === 'function')) {
value.then.call(value, resolve);
return;
}
state = 1;
resultValue = value;
setTimeout(function(){
callbacks.forEach(function (cb) {
handleCallBack(cb);
});
},0);
}
something(resolve);
}
複製程式碼
下面再繼續加入reject
功能。
function Promise(something) {
var callbacks = [];
var state = 0;//0:pending,1:fulfilled 2:reject
var resultValue = null;
this.then = function (onCompelete, onReject) {
return new Promise(function (resolve) {
handleCallBack({
onCompelete: onCompelete,
resolve: resolve,
reject: onReject
});
});
};
function handleCallBack(callback) {
switch (state) {
case 0: {
callbacks.push(callback);
break;
}
case 1: {
var ret = callback.onCompelete(resultValue);
callback.resolve(ret);
break;
}
case 2: {
if(callback.reject){
var ret = callback.reject(resultValue);
}
callback.resolve(ret);
break;
}
default: {
break;
}
}
}
function reject(error) {
state = 2;
resultValue = error;
setTimeout(function () {
callbacks.forEach(function (cb) {
handleCallBack(cb);
});
}, 0);
}
function resolve(value) {
if (value && (typeof value.then === 'function')) {
value.then.call(value, resolve);
return;
}
state = 1;
resultValue = value;
setTimeout(function () {
callbacks.forEach(function (cb) {
handleCallBack(cb);
});
}, 0);
}
something(resolve,reject);
}
複製程式碼
OK,通過上面一步一步對Promise
進行修改,基本上是把Promise
的功能完善了。
從這個上面一步一步剖析Promise
原理的過程中,我們發現,Promise
本身並不提供非同步
功能,Promise
只是對函式的回撥功能進行了封裝,甚至可以理解為Promise
就是一個回撥代理。但是正是有了這個回撥代理,使得我們的回撥方式發生了徹底的改變,甚至直接影響了專案的架構設計。而在平時的開發過程中,Promise
在非同步程式設計中起到了幾乎不可替代的作用。