70行實現Promise核心原始碼
前言:
一直以來都是隻會呼叫Promise的API,而且調API還是呼叫axios封裝好的Promise,太丟人了!!!沒有真正的去了解過它的原理是如何實現的,自己也看過很多博主實現的Promise,但總覺得用原型鏈的OOP晦澀難懂。
個人的理解:如果帶著觀察者模式的想法來理解Promise原始碼,你就會發現Promise本身其實一種微任務的觀察者模式,一個非同步任務的完成,res/rej
的狀態回撥hook => 通知所有then()
訂閱的promise物件。promise只是將觀察者模式運用到微任務。讓promise物件能夠具有很高的優先順序。說到底還是一種解藕的設計模式。
promise是誕生的原因?
在瞭解Promise之前,我覺得有必要去了解一下Promise誕生的原因。 直接就那上面的axios來說吧,以前沒有出現axios的時候,大家是怎麼去與後臺介面做互動的呢? 我當時是用jQuery封裝好的AJAX去做的。下面有一個例子
$.ajax({
type: 'POST', //GET or POST
url: "jquery-ajax",
cache: false,
data: {todo:"ajaxexample1"},
success: functionSucceed,
error: functionFailed,
statusCode: {
404: function() {
alert("page not found");
}
}
});
如果是單獨的一個請求還好,但是如果得傳送兩個相互依賴的請求呢?這時候就會出現回撥地獄的問題,不能自拔。以下就是一個簡單的例子。
a(function (result1) {
b(result1,function (result2) {
c(result2, function (result3) {
d(result3, function (result4) {
e(result4, function (result5) {
console.log(result5)
})
})
})
})
})
假如說讓你去維護一個這樣的程式碼... 害怕的兄弟萌把害怕打在評論區[doge]。上面的程式碼有什麼問題呢?
- 巢狀呼叫,下面的任務依賴上個任務的請求結果。如果2層還是容易理順邏輯,但是一旦出現層數過多,可讀性就會變得非常差,就像一坨屎
- 任務的不確定性。每一個任務會有成功和失敗兩種狀態,就拿上面的程式碼,假如說有5層的巢狀,就要做5次的成功、失敗的判斷函式,明顯的增加了程式碼的複雜度,不符合Unix哲學。
用Typescript實現MyPromise
問題出來了,那解決的思路也有了:
- 消滅巢狀
- 合併多個錯誤
設計一個物件實現上面兩個功能,使用TypeScript的OOP相比於用原型鏈來實現會更加的容易理解。在實現Promise原始碼之前,對於Promise的用法、基本定義一定要有一個全方面的認知,不然去了解Promise也艱深晦澀。可以先去看看MDN對於Promise的本質定義
定義基本的屬性和建構函式
Promise有三種狀態:pending、 resolve、reject 對應了 等待、成功、失敗,表示一個非同步任務的狀態是怎麼樣的。
enum States {
PENDING = 'PENDING',
RESOLVED = 'RESOLVED',
REJECTED = 'REJECTED'
}
class MyPromise {
private state: States = States.PENDING;
private handlers: any[] = [];
private value: any;
constructor(executor: (resolve, reject) => void) {
try {
executor(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
}
handlers陣列是表示當呼叫了then()方法時,向handlers新增回撥函式。比如以下的情況,handlers中就會有兩個回撥函式,等待Promise的resolve/reject設定狀態之後,呼叫handlers裡的所有回撥函式。
let promise1 = new MyPromise(test);
let promise2 = promise1
.then(res => { // <=== 匿名回撥函式
console.log(res);
return 2;
});
let promise3 = promise1
.then(res => { // <=== 匿名回撥函式
setTimeout(() => {
console.log(res + '***********************');
return 4;
}, 1000);
})
value表示的一個非同步函式返回值。
executor是帶有 resolve
和 reject
兩個引數的函式 。Promise建構函式執行時立即呼叫executor
函式, resolve
和 reject
兩個函式作為引數傳遞給executor
(executor 函式在Promise建構函式返回所建promise例項物件前被呼叫)
回到主題,我覺得先介紹then()方法是如何實現的比較合適
實現then()
then(onSuccess?, onFail?) {
return new MyPromise((resolve, reject) => {
return this.attachHandler({
onSuccess: result => {
if (!onSuccess) return resolve(result);
try {
return resolve(onSuccess(result));
} catch (e) {
return reject(e);
}
},
onFail: reason => {
if (!onFail) return reject(reason);
try {
return resolve(onFail(reason));
} catch (e) {
return reject(e);
}
}
});
});
}
then方法的工作原理:返回一個新的Promise物件,且向原Promise物件中的handlers新增一個包含回撥函式的物件,如果Promise處於Settled狀態,那就直接執行回撥函式,否則,得等待Promise的狀態設定。
private execHandlers = () => {
if (this.state === States.PENDING) return;
this.handlers.forEach(handler => {
if (this.state === States.REJECTED) {
return handler.onFail(this.value);
}
return handler.onSuccess(this.value);
});
this.handlers = [];
};
private attachHandler = (handler: any) => {
this.handlers.push(handler);
this.execHandlers();
};
實現resolve和reject回撥函式
按照原生的Promise.then()方法的邏輯來講,原Promise的狀態會直接影響到then
方法返回的Promise的狀態,因此設定狀態的resolve
和reject
函式邏輯如下:
private resolve = value => this.setResult(value, States.RESOLVED);
private reject = value => this.setResult(value, States.REJECTED);
private setResult = (value, state: States) => {
const set = () => {
if (this.state !== States.PENDING) return;
this.value = value;
this.state = state;
return this.execHandlers();
};
setTimeout(set, 0);
};
因為無法實現真正的Promise的微任務,因此只能夠通過setTimeout(fn,0),勉強來模擬實現
private resolve = value => this.setResult(value, States.RESOLVED);
private reject = value => this.setResult(value, States.REJECTED);
private setResult = (value, state: States) => {
const set = () => {
if (this.state !== States.PENDING) return;
this.value = value;
this.state = state;
return this.execHandlers();
};
setTimeout(set, 0);
};
離真正的微任務在一些特別的程式碼上還是有很大差距的,因為setTimeout是巨集任務,在execHandlers
方法中通過foreach 執行本次Promise的handlers中的回撥函式時,處於同一個事件迴圈,以下的程式碼就會和真正的Promise有出入。
function test(res, rej) {
console.log('executor');
setTimeout(() => {
console.log('Timer');
res(1);
}, 1000);
}
console.log('start');
let promise1 = new MyPromise(test); // <== 這裡替換成原生的Promise,會發現promise2狀態不同
let promise2 = promise1.then(res => {
console.log(res);
return 2;
});
let promise3 = promise1.then(res => {
console.log(promise2); //原生的狀態是resolve,MyPromise的狀態是pending
});
console.log('end');
總結
就拿上面的demo來理解整個Promise幫助我們做了什麼吧!
- 控制檯輸出'start '
- 建立MyPromise物件並且執行test函式,引用賦值於promise1=> 輸出'executor',向延遲佇列新增等待1s的回撥函式
- 呼叫promise1
then
方法 => 建立一個新的promise例項賦於給promise2,並且新的promise例項的executor
執行promise1的attachHandler
,將then函式中的回撥函式物件push進promise1的handlers
屬性中,如果promise1已經是settled狀態,直接更加promise1的狀態來執行不同函式 - promise3同promise2一樣的道理,這時promise1的
handlers
陣列中有兩個持有回撥函式的物件,這兩個Promise3和promise2都等著promise1的setResult
來執行相應的回撥,因此promise3和promise此時屬於pending狀態 - 控制檯輸出'end'
- 等待1秒,控制檯輸出'Timer' ,呼叫Promise1的resolve函式,向微任務佇列新增setResult狀態函式,MyPromise使用settimeout模擬微任務佇列
- setResult狀態函式根據
res/rej
狀態執行handlers中的所有then新增的回撥,
Promise類的catch
和finally
都是在then上建立的語法糖,具體大家可以更具MDN的定義來實現,還有Promise類的靜態方法,可以參考我自己GitHub的實現。
不斷的沉澱下來,總歸會理解一個東西存在的意義。理解了promise的原理之後,去理解其他的底層實現有會一個很好的基礎,瞭解了Promise底層之後,深深的感受到設計模式的強大。
如果小夥伴們覺得不錯的話,點贊支援一下嗷 鐵汁~
以下是用Typescript實現的MyPromise原始碼,不過引數並沒有用型別,所以稱作es6的class語法也不為過。
enum States {
PENDING = 'PENDING',
RESOLVED = 'RESOLVED',
REJECTED = 'REJECTED'
}
export class MyPromise {
private state: States = States.PENDING;
private handlers: any[] = [];
private value: any;
constructor(callback: (resolve, reject) => void) {
try {
callback(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
private resolve = value => this.setResult(value, States.RESOLVED);
private reject = value => this.setResult(value, States.REJECTED);
private setResult = (value, state: States) => {
const set = () => {
if (this.state !== States.PENDING) return;
this.value = value;
this.state = state;
return this.execHandlers();
};
setTimeout(set, 0);
};
private execHandlers = () => {
if (this.state === States.PENDING) return;
this.handlers.forEach(handler => {
if (this.state === States.REJECTED) {
return handler.onFail(this.value);
}
return handler.onSuccess(this.value);
});
this.handlers = [];
};
private attachHandler = (handler: any) => {
this.handlers.push(handler);
this.execHandlers();
};
then(onSuccess?, onFail?) {
return new MyPromise((resolve, reject) => {
return this.attachHandler({
onSuccess: result => {
if (!onSuccess) return resolve(result);
try {
return resolve(onSuccess(result));
} catch (e) {
return reject(e);
}
},
onFail: reason => {
if (!onFail) return reject(reason);
try {
return resolve(onFail(reason));
} catch (e) {
return reject(e);
}
}
});
});
}
}
參考連結
https://www.freecodecamp.org/news/how-to-implement-promises-in-javascript-1ce2680a7f51/
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise