支援鏈式操作
我們平時寫promise一般都是對應的一組流程化的操作,如這樣: promise.then(f1).then(f2).then(f3) 但是我們之前的版本最多隻能註冊一個回撥,這一節我們就來實現鏈式操作。
目標
使promise支援鏈式操作
實現
想支援鏈式操作,其實很簡單,首先儲存回撥時要改為使用陣列
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
複製程式碼
當然執行回撥時,也要改成遍歷回撥陣列執行回撥函式
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
複製程式碼
最後,then方法也要改一下,只需要在最後一行加一個return this即可,這其實和jQuery鏈式操作的原理一致,每次呼叫完方法都返回自身例項,後面的方法也是例項的方法,所以可以繼續執行。
Promise.prototype.then = function(onFulfilled, onRejected){
let self = this;
if(self.state === 'resolved'){
onFulfilled(self.value);
}
if(self.state === 'rejected'){
onRejected(self.reson);
}
if(self.state === 'padding'){
self.onResolvedCallbacks.push(function(){
onFulfilled(self.value);
});
self.onRejectedCallbacks.push(function(){
onRejected(self.reson);
});
}
return this;
}
複製程式碼
這種簡單返回this會有一個問題,這裡為方便講解我們引入一個常見場景:用promise順序讀取檔案內容,場景程式碼如下:
let p = new Promise((resolve, reject) => {
fs.readFile('../file/1.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
let f1 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/2.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f2 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/3.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f3 = function(data) {
console.log(data);
}
let errorLog = function(error) {
console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)
複製程式碼
上面場景,我們讀取完1.txt後並列印1.txt內容,再去讀取2.txt並列印2.txt內容,再去讀取3.txt並列印3.txt內容,而讀取檔案都是非同步操作,所以都是返回一個promise,我們上一節實現的promise可以實現執行完非同步操作後執行後續回撥,但是本節的回撥讀取檔案內容操作並不是同步的,而是非同步的,所以當讀取完1.txt後,執行它回撥onFulfilledCallbacks裡面的f1,f2,f3時,非同步操作還沒有完成,所以我們本想得到這樣的輸出:
this is 1.txt
this is 2.txt
this is 3.txt
複製程式碼
但是實際上卻會輸出
this is 1.txt
this is 1.txt
this is 1.txt
複製程式碼
所以要想實現非同步操作序列,我們不能將回撥函式都註冊在初始promise的onFulfilledCallbacks裡面,而要將每個回撥函式註冊在對應的非同步操作promise的onFulfilledCallbacks裡面,用讀取檔案的場景來舉例,f1要在p的onFulfilledCallbacks裡面,而f2應該在f1裡面return的那個Promise的onFulfilledCallbacks裡面,因為只有這樣才能實現讀取完2.txt後才去列印2.txt的結果。
但是,我們平常寫promise一般都是這樣寫的: promise.then(f1).then(f2).then(f3),一開始所有流程我們就指定好了,而不是在f1裡面才去註冊f1的回撥,f2裡面才去註冊f2的回撥。
如何既能保持這種鏈式寫法的同時又能使非同步操作銜接執行呢?我們其實讓then方法最後不再返回自身例項,而是返回一個新的promise即可,我們可以叫它bridgePromise,它最大的作用就是銜接後續操作,我們看下具體實現程式碼:
function Promise(executor){
let self = this;
self.state = 'padding';
self.value = undefined;
self.reason = undefined;
self.onResolvedCallbacks = [];
self.onRejectedCallbacks = [];
function resolve(value){
if(self.state == 'padding'){
self.state = 'resolved'
self.value = value
self.onResolvedCallbacks.forEach(fn=>fn());
}
}
function reject(reason){
if(self.state == 'padding'){
self.state = 'rejected'
self.reason = reason
self.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
executor(resolve, reject);
}catch(e){
reject(e);
}
}
Promise.prototype.then = function(onFulfilled, onRejected){
let self = this;
//不能寫成:let promise2 = new Promise(function(resolve, reject){}}
let promise2;
promise2 = new Promise(function(resolve, reject){
if(self.state === 'resolved'){
try{
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
}catch(e){
reject(e);
}
}
if(self.state === 'rejected'){
try{
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(e){
reject(e);
}
}
if(self.state === 'padding'){
self.onResolvedCallbacks.push(function(){
try{
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
}catch(e){
reject(e);
}
});
self.onRejectedCallbacks.push(function(){
try{
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(e){
reject(e);
}
});
}
});
return promise2;
}
/**
* [resolvePromise description]
* @param {[type]} promise2 [then的返回值(新的promise)]
* @param {[type]} x [then種成功或失敗的返回值]
* @param {[type]} resolve [promise2中的resolve]
* @param {[type]} reject [promise2中的reject]
* @return {[type]} [description]
*/
function resolvePromise(promise2, x, resolve, reject){
//console.log('resolvePromise');
if(promise2 === x){
return reject(new TypeError('迴圈引用promise'));
}
if(x!==null && (typeof x === 'object' || typeof x === 'function')){
let then = x.then;
if(typeof then === 'function'){//說明返回的是promise,只有promise上才有then
console.log('function');
then.call(
x,
y=>resolve(y),
err=>reject(err)
);
}else{//說明返回的是 普通物件
console.log('object');
resolve(x);
}
}else{//說明返回的是 普通值
resolve(x);
}
}
複製程式碼
這裡很抽象,我們還是以檔案順序讀取的場景畫一張圖解釋一下流程:
當執行p.then(f1).then(f2).then(f3)時:
- 先執行p.then(f1)返回了一個bridgePromise(p2),並在p的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f1並且更新p2的狀態.
- 然後.then(f2)時返回了一個bridgePromise(p3),這裡注意其實是p2.then(f2),因為p.then(f1)時返回了p2。此時在p2的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f2並且更新p3的狀態.
- 然後.then(f3)時返回了一個bridgePromise(p4),並在p3的onFulfilledCallbacks回撥列表中放入一個回撥函式,回撥函式負責執行f3並且更新p4的狀態. 到此,回撥關係註冊完了,如圖所示:
-
然後過了一段時間,p裡面的非同步操作執行完了,讀取到了1.txt的內容,開始執行p的回撥函式,回撥函式執行f1,列印出1.txt的內容“this is 1.txt”,並將f1的返回值放到resolvePromise中開始解析。resolvePromise一看傳入了一個promise物件,promise是非同步的啊,得等著呢,於是就在這個promise物件的then方法中繼續resolvePromise這個promise物件resolve的結果,一看不是promise物件了,而是一個具體值“this is 2.txt”,於是呼叫bridgePromise(p2)的reslove方法將bridgePromise(p2)的狀態更新為fulfilled,並將“this is 2.txt”傳入p2的回撥函式中去執行。
-
p2的回撥開始執行,f2拿到傳過來的“this is 2.txt”引數開始執行,列印出2.txt的內容,並將f2的返回值放到resolvePromise中開始解析,resolvePromise一看傳入了一個promise物件,promise是非同步的啊,又得等著呢........後續操作就是不斷的重複4,5步直到結束。