原理分析
場景分析
//例1
function getUserId() {
return new Promise(function(resolve) {
//異步請求
http.get(url, function(results) {
resolve(results.id)
})
})
}
getUserId().then(function(id) {
//一些處理
})
簡易版promise
function Promise(fn) {
var value = null;
var callbacks = [];
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
} //注冊回調(diào)函數(shù)。
function resolve (value) {
callbacks.forEach((callback) => {
callback(value);
})// 當(dāng)Promise中觸發(fā)了resolve()時候。執(zhí)行回調(diào)函數(shù)
fn(resolve); // 執(zhí)行Promise傳入的函數(shù)。
}
}
上述代碼很簡單,大致的邏輯是這樣的:
- 調(diào)用
then方法,將想要在Promise異步操作成功時執(zhí)行的回調(diào)放入callbacks,其實(shí)也就是注冊回調(diào)函數(shù),可以向觀察者模式方向思考; - 創(chuàng)建
Promise實(shí)例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù),即resolve,它接收一個參數(shù)value,代表異步操作返回的結(jié)果,當(dāng)一步操作執(zhí)行成功后,用戶會調(diào)用resolve方法,這時候其實(shí)真正執(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)用,其實(shí)也是很簡單的:
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};
只要簡單一句話就可以實(shí)現(xiàn)類似下面的鏈?zhǔn)秸{(diào)用:
// 例2
getUserId().then(function (id) {
// 一些處理
}).then(function (id) {
// 一些處理
});
不過上式中的then方法雖然是鏈?zhǔn)秸{(diào)用但卻全部都是注冊給getUserId這個promise的。
加入延時機(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)后的代碼是這樣的:
// promise 1.0
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í)行。
鏈?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)?code>Promise。
鏈?zhǔn)?code>Promise是指在當(dāng)前promise達(dá)到fulfilled狀態(tài)后,即開始進(jìn)行下一個promise(后鄰promise)。那么我們?nèi)绾毋暯赢?dāng)前promise和后鄰promise呢?(這是這里的難點(diǎn))。
之前實(shí)現(xiàn)的的promise實(shí)現(xiàn)為什么不能連接前后的promise呢?
之前是return this實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。這樣的話
function getUserJobById() {
return new Promise(function (resolve) {
setTimeout(()=>{resolve('2')},2000)
})
}
function getUserId() {
return new Promise(function (resolve) {
setTimeout(()=>{resolve('1')},2000)
});
}
getUserId()
.then(getUserJobById)
.then(function (job) {
console.log('job', job) // 打印出來的結(jié)果為1
});
打印的結(jié)果為1,是因?yàn)閮蓚€then函數(shù)都注冊的是getUserId的回調(diào)。所以getUserId觸發(fā)resolve('1')時候,兩個then函數(shù)注冊的函數(shù)都會被執(zhí)行。所以這里我們打印的結(jié)果為1。這并不是我們想要的結(jié)果。我們所期望的結(jié)果是第二個then函數(shù)能在第一個then函數(shù)執(zhí)行完成后再執(zhí)行。就是getUserId和getUserJobById兩個promise都觸發(fā)了resolve后執(zhí)行。
那我們把 return this給去掉。不把一個promise的回調(diào)寫在多個then函數(shù)里,我們都寫在一個then函數(shù)里。試試結(jié)果如何?
VM707:34 Uncaught TypeError: Cannot read property 'then' of undefined
at <anonymous>:34:5 //報錯
為什么會報錯呢。因?yàn)闆]有了return this之后,第一個then函數(shù)執(zhí)行后的沒有任何返回值,所以第二個then函數(shù)of undefined了。
所以解決辦法可以是給 then函數(shù)返回一個promise就好啦。這樣可以統(tǒng)一then里面是同步或者異步不同的執(zhí)行順序鏈接,并且這樣的話,如果then函數(shù)(稱為為前一個then,就是getUserId().then()的這個then)里面還是個promise例如getUserJobById的話 ,可以getUserJobById函數(shù)resolve時執(zhí)行自身promise的then函數(shù)(也是一個promise)的resolve,同時觸發(fā)前一個then函數(shù)的resolve(就是一個then內(nèi)手動的觸發(fā)兩個promise函數(shù)resolve)。這樣的話就可以執(zhí)行前一個then(promise)函數(shù)的的注冊(下一個)then函數(shù)了。如果then函數(shù)中不是promise的話。就直接執(zhí)行完觸發(fā)then函數(shù)的resolve。
例如getUserId().then(getUserJobById)中的then函數(shù)是一個promise。這個then的promise的resolve事件會在getUserJobById這個promise函數(shù)resolve后進(jìn)行。
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;
}
var ret = callback.onFulfilled(value);
callback.resolve(ret);// 因?yàn)閠hen現(xiàn)在是promise,所以需要這一步把then的promise給結(jié)束掉
}
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的代碼,分析下上面的代碼邏輯,
-
then方法中,創(chuàng)建并返回了新的Promise實(shí)例,這是串行Promise的基礎(chǔ),并且支持鏈?zhǔn)秸{(diào)用。 -
handle方法是promise內(nèi)部的方法。then方法傳入的形參onFulfilled以及創(chuàng)建新Promise實(shí)例時傳入的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。這種情況下,會將該then生成的promise的resolve方法傳入getUserJobById promise的then方法中,并直接返回。 - 在
getUserJobById promise異步操作成功時,執(zhí)行其callbacks``中的回調(diào)getUserId bridge promise中的resolve`方法 - 最后執(zhí)行
getUserId bridge promise的后鄰promise的callbacks中的回調(diào)。
30分鐘,讓你徹底明白Promise原理
[Promise 的鏈?zhǔn)秸{(diào)用與中止](http://www.tuicool.com/articles/eERRZjA
PromiseA的resolve里面如果還是個PromiseB。不論這個PromiseB是否已經(jīng)被決議,都會把PromiseA的resolve放在隊列的最后。規(guī)定的行為是把 p3 展開到 p1,但是是異步地展開。
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );
p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );
p1.then( function(v){
console.log( v );
} );
p2.then( function(v){
console.log( v );
} );
// A B <-- 而不是像你可能認(rèn)為的B A
