Promise是什麼?
在Javascript的世界中,程式碼都是單執行緒執行的。這就導致我們的程式碼裡會有巢狀回撥函式,一旦巢狀過多,導致程式碼不容易理解和維護。
為了降低非同步程式設計的複雜性,開發人員一直在尋找各種解決方案,Promise只是用來處理非同步操作的其中一個方案。
下面我就結合著Promise的使用場景,來逐一手寫Promise的實現原理。
因為ES6最終採用了Promise/A+ 規範,下面所有程式碼都是基於Promise/A+規範來實現。有興趣的同學可以檢視 Promise/A+規範 https://promisesaplus.com/
Promise的基本使用:
let promise = new Promise((resolve,reject)=>{
resolve('success'); //這裡如果是reject('fail')
});
promise.then((res)=>{
console.log(res); // 輸出:success
},(err)=>{
console.log(err); // 上面如果執行reject('fail') 這裡就輸出:fail
});
複製程式碼
看圖就很好理解了,resolve找then裡的成功回撥,reject找then裡失敗的回撥。
那麼如何用我們自己的方式一步步實現這樣的一個Promise類呢?直接上程式碼吧,我會註釋得稍微詳細點。
class Promise { //建立一個Promise類
constructor(executor) {
this.status = 'pending'; //初始預設狀態為pending
this.value = undefined; //預設賦值為undefined
this.reason = undefined; //預設賦值為undefined
let resolve = (value) => {
if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
this.value = value; //將傳遞進來的的值賦給value儲存
this.status = 'resolved'; //將狀態設定成resolved
}
}
let reject = (reason) => {
if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
this.reason = reason; //將傳遞進來的失敗原因賦給reason儲存
this.status = 'rejected'; //將狀態設定成rejected
}
}
executor(resolve, reject); //預設執行executor
}
then(onFulfilled, onRejected) { //等同於es5的Promise.prototype.then 當呼叫then的時候,根據狀態,來執行不同的函式
if (this.status === 'resolved') { //如果狀態是resolved
onFulfilled(this.value); //執行成功的resolve,並將成功後的值傳遞過去
}
if (this.status === 'rejected') { //如果狀態是rejected
onRejected(this.reason); //執行失敗的reject,並將失敗原因傳遞過去
}
}
}
module.exports = Promise; //將Promise匯出
複製程式碼
現在只要在js程式碼中引入Promise庫,就能實現最基本的Promise功能了。
輸出結果:success
還有一點需要注意,就是當程式碼出現錯誤的情況下,我們需要能夠捕獲錯誤,並且處理錯誤。所以在executor(resolve, reject)這一行程式碼需要處理下,包上一層try/catch,如下:
try {
executor(resolve, reject);
} catch (e) {
reject(e); //如果發生錯誤,將錯誤放入reject中
}
複製程式碼
setTimeout呼叫問題
此時的Promise類可以實現呼叫resolve,也可以呼叫reject。呼叫then之後都會執行相應的函式。看似已經沒有問題了。但如果將resolve放在一個定時器裡呢?看看下面的程式碼:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('setTimeout');
}, 500);
})
promise.then((res) => {
console.log(res);
}, (err) => {
console.log(err);
})
複製程式碼
我們會發現上面的程式碼在控制檯完全沒有反應,這是為什麼呢?實際上是因為我們壓根就沒有處理這種延遲呼叫的情況。現在我們來處理以下:
class Promise { //建立一個Promise類
constructor(executor) {
...此處略去部分程式碼
this.successStore = []; //定義一個存放成功函式的陣列
this.failStore = []; //定義一個存放失敗函式的陣列
let resolve = (value) => {
if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
...此處略去部分程式碼
this.successStore.forEach(fnc => fnc()); //一次執行陣列中的成功函式
}
}
let reject = (reason) => {
if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
...此處略去部分程式碼
this.failStore.forEach(fnc => fnc()) //依次執行陣列中的失敗函式
}
}
...此處略去部分程式碼
}
then(onFulfilled, onRejected) { //等同於es5的Promise.prototype.then 當呼叫then的時候,根據狀態,來執行不同的函式
...此處略去部分程式碼
if (this.status === 'pending') { //此處增加一種狀態判斷
this.successStore.push(() => { //當狀態為pending時將成功的函式存放到陣列裡
onFulfilled(this.value);
})
this.failStore.push(() => { //當狀態為pending時將失敗的函式存放到陣列中
onRejected(this.reason);
})
}
}
}
module.exports = Promise; //將Promise匯出
複製程式碼
這樣就解決了setTimeout呼叫的問題了。
then的鏈式呼叫
細心的同學肯定看到了,在我們上面的程式碼中,其實並沒有處理then的鏈式呼叫。如果此時使用then()方法的鏈式呼叫,會報錯:TypeError: Cannot read property 'then' of undefined。Promise/A+規範裡說了,then()方法返回的必須是Promise例項,這裡我們可以理解成返回的必須是一個新的Promise例項。所以then()方法後面可以繼續跟另一個then()方法進行鏈式呼叫。
但是then()方法裡可能發揮一個值或者返回的是一個Promise例項,這就需要我們分別處理這兩種情況。
第一種:
let promise = new Promise((resolve, reject) => {
resolve('success');
})
p.then((res)=>{
console.log(res); // success
return 'hello world!'
},(err)=>{
console.log(err);
}).then((res)=>{
console.log(res); // hello world
},(err)=>{
console.log(err);
})
複製程式碼
在then()方法的回撥裡返回一個普通值,無論是成功還是失敗的回撥,都會進入到下一個then()的成功態裡。如下:
let promise = new Promise((resolve, reject) => {
reject('fail');
})
p.then((res)=>{
console.log(res);
},(err)=>{
console.log(err); //fail
return 'fail';
}).then((res)=>{
console.log(res); //fail 注意:此時走的是成功回撥,並非失敗的回撥
},(err)=>{
console.log(err);
})
複製程式碼
第二種:
let promise = new Promise((resolve, reject) => {
resolve();
})
promise.then((res)=>{
return new Promise((resolve,reject)=>{ //返回一個新的Promise
resolve('hello world');
})
},(err)=>{
console.log(err);
}).then((res)=>{
console.log(res); //hello world
},(err)=>{
console.log(err);
})
複製程式碼
這兩種情況都要考慮到,我們修改下程式碼:
function handlePromise(promise2, x, resolve, reject) {
if (promise2 === x) { //promise2是否等於x,也就是判斷是否將自己本身返回
return reject(new TypeError('circular reference')); //如果是丟擲錯誤
}
//判斷x不是bull且x是物件或者函式
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called; //called控制resolve或reject 只執行一次,多次呼叫沒有任何作用。
try {
let then = x.then; //取x.then()方法
if (typeof then === 'function') { //如果是函式,就認為它是返回新的promise
then.call(x, y => { //如果y是promise繼續遞迴解析
if (called) return;
called = true;
handlePromise(promise2, y, resolve, reject); //遞迴解析promise
}, r => {
if (called) return;
called = true;
reject(r)
})
} else { //不是函式,就是普通物件
resolve(x); //直接將物件返回
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else { //x是普通值,直接走then的成功回撥
resolve(x);
}
}
class Promise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.successStore = [];
this.failStore = [];
let resolve = (value) => {
if (this.status === 'pending') {
this.value = value;
this.status = 'resolved';
this.successStore.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.reason = reason;
this.status = 'rejected';
this.failStore.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) { //原型上的方法
let promise2; // 返回的新的promise
if (this.status === 'resolved') {
promise2 = new Promise((resolve, reject) => {
try {
let x = onFulfilled(this.value);
handlePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) => {
try {
let x = onRejected(this.reason); //x存放返回的結果
handlePromise(promise2, x, resolve, reject); //處理返回結果的函式,已經在上面定義
} catch (e) {
reject(e);//報錯執行reject
}
})
}
if (this.status === 'pending') {
promise2 = new Promise((resolve, reject) => {
this.successStore.push(() => {
try {
let x = onFulfilled(this.value); //x存放返回的結果
handlePromise(promise2, x, resolve, reject);//處理返回結果的函式,已經在上面定義
} catch (e) {
reject(e); //報錯執行reject
}
})
this.failStore.push(() => {
try {
let x = onRejected(this.reason);//x存放返回的結果
handlePromise(promise2, x, resolve, reject);//處理返回結果的函式,已經在上面定義
} catch (e) {
reject(e);//報錯執行reject
}
})
})
}
return promise2; //返回新的promise
}
}
module.exports = Promise;
複製程式碼
處理值穿透
先看例項
let promise = new Promise((resolve, reject)=>{
resolve('hello world');
})
promise.then().then().then((res)=>{
console.log(res);//我們希望可以正常列印出hello world,如何處理呢?
})
複製程式碼
只需要在then的原型方法上加上一層判斷,判斷then裡是否傳遞裡函式,如果沒有傳遞,我們手動傳遞一個函式,並讓值返回。
then(onFulfilled, onRejected) { //原型上的方法
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y; //判斷是否是一個函式
onRejected = typeof onRejected === 'function' ? onRejected : errr => { //判斷是否是一個函式
throw err; //注意,這裡不是返回值,而是丟擲錯誤
}
}
複製程式碼
這樣當我們呼叫Promise時就可以正常列印出hello world了。
新增catch方法
下面我們繼續新增catch方法
then(onFulfilled, onRejected){
...
}
catch(onRejected){ //在此處新增原型上的方法catch
return this.then(null,onRejected);
}
複製程式碼
catch()其實就是簡易版的then()方法,它沒有成功的回撥,只有失敗的回撥。所以按照上面的方式處理下就可以了。
新增Promise.all方法
Promise.all = function (promiseArrs) { //在Promise類上新增一個all方法,接受一個傳進來的promise陣列
return new Promise((resolve, reject) => { //返回一個新的Promise
let arr = []; //定義一個空陣列存放結果
let i = 0;
function handleData(index, data) { //處理資料函式
arr[index] = data;
i++;
if (i === promiseArrs.length) { //當i等於傳遞的陣列的長度時
resolve(arr); //執行resolve,並將結果放入
}
}
for (let i = 0; i < promiseArrs.length; i++) { //迴圈遍歷陣列
promiseArrs[i].then((data) => {
handleData(i, data); //將結果和索引傳入handleData函式
}, reject)
}
})
}
複製程式碼
下面我們逐個新增reace、resolve和reject方法。
新增Promise.race方法
race比賽競賽的意思,也就是誰跑的快就返回誰,不管你是成功還是失敗。跟all方法比較相似,但更簡單一些。循壞之後直接
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(resolve, reject);
}
})
}
複製程式碼
經過上面的程式碼,以下兩個方法就顯得很簡單了,直接返回一個新的Promise,然後執行成功/失敗既可。
新增Promise.resolve方法
Promise.resolve = function (val) {
return new Promise((resolve, reject) => resolve(val));
}
複製程式碼
新增Promise.reject方法
Promise.reject = function (val) {
return new Promise((resolve, reject) => reject(val));
}
複製程式碼
如果想要用promises-aplus-tests測試是否符合Promise/A+規範的話,就要加上一段程式碼:
Promise.deferred = Promise.defer = function () { //這是promise的語法糖
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
複製程式碼
在終端安裝完promises-aplus-tests後,在檔案的終端下執行promises-aplus-tests "檔名"既可
npm install promises-aplus-tests -g
promises-aplus-tests filename
複製程式碼
最後,解決下非同步的問題,完整程式碼如下:
function handlePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('circular reference'));
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called;
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
handlePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r)
})
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
class Promise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.successStore = [];
this.failStore = [];
let resolve = (value) => {
if (this.status === 'pending') {
this.value = value;
this.status = 'resolved';
this.successStore.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.reason = reason;
this.status = 'rejected';
this.failStore.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y;
onRejected = typeof onRejected === 'function' ? onRejected : errr => {
throw err;
}
let promise2;
if (this.status === 'resolved') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //非同步處理
try {
let x = onFulfilled(this.value);
handlePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //非同步處理
try {
let x = onRejected(this.reason);
handlePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
if (this.status === 'pending') {
promise2 = new Promise((resolve, reject) => {
this.successStore.push(() => {
setTimeout(() => { //非同步處理
try {
let x = onFulfilled(this.value);
handlePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
this.failStore.push(() => {
setTimeout(() => { //非同步處理
try {
let x = onRejected(this.reason);
handlePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
})
}
return promise2;
}
}
Promise.all = function (promiseArrs) {
return new Promise((resolve, reject) => {
let arr = [];
let i = 0;
function processData(index, data) {
arr[index] = data;
i++;
if (i === promiseArrs.length) {
resolve(arr);
}
}
for (let i = 0; i < promiseArrs.length; i++) {
promiseArrs[i].then((data) => {
processData(i, data);
}, reject)
}
})
}
Promise.deferred = Promise.defer = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
module.exports = Promise;
複製程式碼
現在,一個符合Promise/A+規範的Promise類已經完成了。見解有限,如有描述不準確之處,請幫忙及時指出。如有錯誤,一定會及時更正。
參考
segmentfault.com/a/119000000…
promisesaplus.com/
zhuanlan.zhihu.com/p/25178630