前言

Promise 作為一個前端必備技能,不管是從專案應用還是面試,都應該對其有所瞭解與使用。

常常遇到的面試五連問:

  • 說說你對 Promise 理解?
  • Promise 的出現解決了什麼問題?
  • Promise 有哪些狀態?
  • 你用過 Promise 哪些方法?
  • 如何實現一個 Promise ?

什麼是 Promise?

Promise 是非同步程式設計的一種解決方案:從語法上講,promise 是一個物件,從它可以獲取非同步操作的訊息;從本意上講,它是承諾,承諾它過一段時間會給你一個結果。

Promise 有三種狀態:pending(等待態),fulfiled(成功態),rejected(失敗態);狀態一旦改變,就不會再變。創造 Promise 例項後,它會立即執行。

一般來說我們會碰到的回撥巢狀都不會很多,一般就一到兩級,但是某些情況下,回撥巢狀很多時,程式碼就會非常繁瑣,會給我們的程式設計帶來很多的麻煩,這種情況俗稱——回撥地獄。

這時候我們的Promise 就應運而生、粉墨登場了。

Promise 的基本使用

Promise 是一個建構函式,自己身上有 allrejectresolve 這幾個眼熟的方法,原型上有 thencatch 等同樣很眼熟的方法。

首先建立一個 new Promise 例項

let p = new Promise((resolve, reject) => {
// 可以做一些非同步操作,例如傳送AJAX請求,這裡使用 setTimeout 模擬
setTimeout(() => {
let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
if (num <= 5) {
resolve('成功');
} else {
reject('失敗');
}
}, 2000);
});

Promise 的建構函式接收一個引數:函式,並且這個函式需要傳入兩個引數:

resolve:非同步操作執行成功後的回撥函式;

reject:非同步操作執行失敗後的回撥函式;

then 鏈式操作的用法

p.then((data) => {
console.log('resolve', data); // 'resolve', '成功'
}, (err) => {
console.log('reject', err); // 'reject', '失敗'
})

then 中傳了兩個引數,then 方法可以接受兩個引數,第一個對應 resolve 的回撥,第二個對應reject 的回撥。所以我們能夠分別拿到他們傳過來的資料。多次執行這段程式碼,你會隨機得到兩種結果。

catch 的用法

我們知道Promise 物件除了then 方法,還有一個catch 方法,它是做什麼用的呢?其實它和then 的第二個引數一樣,用來指定reject 的回撥。用法是這樣:

p.then((data) => {
console.log('resolve', data); // 'resolve', '成功'
}).catch((err) => {
console.log('reject', err); // 'reject', '失敗'
});

效果和寫在 then 的第二個引數裡面一樣。不過它還有另外一個作用:在執行 resolve 的回撥(也就是上面then 中的第一個引數)時,如果丟擲異常了(程式碼出錯了),那麼並不會報錯卡死js,而是會進到這個 catch 方法中。請看下面的程式碼:

p.then((data) => {
console.log('resolve', data);
console.log(test);
}).catch((err) => {
console.log('reject', err);
}); // 當成功時先後列印
resolve 成功
reject ReferenceError: test is not defined

resolve 的回撥中,我們 console.log(test),而 test 這個變數是沒有被定義的。如果我們不用 Promise,程式碼執行到這裡就直接在控制檯報錯了,不往下運行了,也就是說進到catch方法裡面去了,而且把錯誤原因傳到了 reject 引數中。即便是有錯誤的程式碼也不會報錯了,這與我們的 try/catch 語句有相同的功能。

finally 的用法

finally 方法返回一個 Promise。在 promise 結束時,無論結果是 fulfilled 或者是 rejected ,都會執行指定的回撥函式。這為在 Promise 是否成功完成後都需要執行的程式碼提供了一種方式。

這避免了同樣的語句需要在 thencatch 中各寫一次的情況。

// 載入 loading

let isLoading = true;

// 假裝模擬AJAX請求
function myRequest(){
return new Promise((resolve, reject) => {
setTimeout(() => {
let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
if (num <= 5) {
resolve('成功');
} else {
reject('失敗');
}
}, 1000);
})
} myRequest().then(function(data) { console.log(data); })
.catch(function(error) { console.log(error); })
.finally(function() {
// 關閉loading
isLoading = false;
console.log('finally');
});

resolve 的用法

Promise.resolve(value) 方法返回一個以給定值解析後的 Promise 物件。如果這個值是一個 promise ,那麼將返回這個 promise ;如果這個值是 thenable(即帶有"then" 方法),返回的promise會“跟隨”這個 thenable 的物件,採用它的最終狀態;否則返回的promise將以此值完成。此函式將類promise物件的多層巢狀展平。

// 示例 1 基本使用

const p = Promise.resolve("Success");

p.then(function(value) {
console.log(value); // "Success"
}, function(value) {
// 不會被呼叫
}); // 示例 2 resolve 一個數組 var p = Promise.resolve([1,2,3]);
p.then(function(arr) {
console.log(arr[0]); // 1
}); // 示例 3 resolve 另一個Promise let original = Promise.resolve(33);
let cast = Promise.resolve(original);
cast.then(function(value) {
console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast)); /*
* 列印順序如下,這裡有一個同步非同步先後執行的區別
* original === cast ? true
* value: 33
*/ // 示例 4 resolve thenable 並丟擲錯誤 // Resolve一個thenable物件 const p1 = Promise.resolve({
then: function(onFulfill, onReject) { onFulfill("fulfilled!"); }
});
console.log(p1 instanceof Promise) // true, 這是一個Promise物件 p1.then(function(v) {
console.log(v); // 輸出"fulfilled!"
}, function(e) {
// 不會被呼叫
}); // Thenable在callback之前丟擲異常 // Promise rejects const thenable = { then: function(resolve) {
throw new TypeError("Throwing");
resolve("Resolving");
}}; const p2 = Promise.resolve(thenable); p2.then(function(v) {
// 不會被呼叫
}, function(e) {
console.log(e); // TypeError: Throwing
}); // Thenable在callback之後丟擲異常 // Promise resolves const thenable = { then: function(resolve) {
resolve("Resolving");
throw new TypeError("Throwing");
}}; const p3 = Promise.resolve(thenable); p3.then(function(v) {
console.log(v); // 輸出"Resolving"
}, function(e) {
// 不會被呼叫
});

reject 的用法

Promise.reject()方法返回一個帶有拒絕原因的Promise物件。

Promise.reject(new Error('fail')).then(function() {
// not called
}, function(error) {
console.error(error); // Stacktrace
});

all 的用法

誰跑的慢,以誰為準執行回撥。all 接收一個數組引數,裡面的值最終都算返回 Promise 物件。

Promise.all 方法提供了並行執行非同步操作的能力,並且在所有非同步操作執行完後才執行回撥。看下面的例子:

let p = new Promise((resolve, reject) => {
setTimeout(() => {
let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
if (num <= 5) {
resolve('成功');
} else {
reject('失敗');
}
}, 1000);
}); let pAll = Promise.all([p, p, p]); pAll.then((data) => {
console.log(data); // 成功時列印: ['成功', '成功', '成功']
}, (err) => {
console.log(errs); // 只要有一個失敗,就會返回當前失敗的結果。 '失敗'
})

有了all,你就可以並行執行多個非同步操作,並且在一個回撥中處理所有的返回資料,是不是很酷?有一個場景是很適合用這個的,一些遊戲類的素材比較多的應用,開啟網頁時,預先載入需要用到的各種資源如圖片、flash以及各種靜態檔案。所有的都載入完後,我們再進行頁面的初始化。在這裡可以解決時間效能的問題,我們不需要在把每個非同步過程同步出來。

all 缺點就是隻要有一個任務失敗就會都失敗。

allSettled 的用法

Promise.allSettled ****方法返回一個在所有給定的 promise 都已經fulfilledrejected後的 promise,並帶有一個物件陣列,每個物件表示對應的 promise 結果。

當您有多個彼此不依賴的非同步任務成功完成時,或者您總是想知道每個promise的結果時,通常使用它。

相比之下,Promise.all 更適合彼此相互依賴或者在其中任何一個reject時立即結束。

const p1 = Promise.resolve(3);
const p2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo')); Promise.allSettled([p1, p2]).
then((results) => results.forEach((result) => console.log(result.status))); // 執行後列印
// "fulfilled"
// "rejected"

any 的用法

Promise.any 接收一個 promise 可迭代物件,只要其中的一個 promise 成功,就返回那個已經成功的 promise 。如果可迭代物件中沒有一個 promise 成功(即所有的 promises 都失敗/拒絕),就返回一個失敗的 promiseAggregateError 型別的例項,它是 Error 的一個子類,用於把單一的錯誤集合在一起。本質上,這個方法和 Promise.all 是相反的。

const pErr = new Promise((resolve, reject) => {
reject("總是失敗");
}); const pSlow = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "最終完成");
}); const pFast = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "很快完成");
}); Promise.any([pErr, pSlow, pFast]).then((value) => {
console.log(value); // '很快完成'
})

race 的用法

Promise.race ****方法返回一個 promise,一旦迭代器中的某個 promise 解決或拒絕,返回的 promise就會解決或拒絕。也可理解 誰跑的快,以誰為準執行回撥。

race 的使用場景:比如我們可以用race 給某個非同步請求設定超時時間,並且在超時後執行相應的操作,程式碼如下:

    // 假設請加某個圖片資源
function requestImg() {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = function() {
resolve(img);
}
// 嘗試輸入假的和真的連結
img.src = '**';
})
}
// 延時函式,用於給請求計時
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('請求超時');
}, 5000)
})
} Promise.race([requestImg(), timeout()]).then((data) => {
console.log(data); // 成功時 img
}).catch((err) => {
console.log(err); // 失敗時 "請求超時"
})

如何實現一個Promise

1、建立一個 MyPromise 類,傳入 executor(執行器)並新增 resolve 和 reject 方法


class MyPromise {
constructor(executor){
// executor 是一個執行器,進入會立即執行
// 並傳入resolve和reject方法
executor(this.resolve, this.reject)
} // 更改成功後的狀態
resolve = () => {}
// 更改失敗後的狀態
reject = () => {}
}

2、新增狀態和 resolve、reject 事件處理

// 定義三種狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'; class MyPromise {
constructor(executor){
executor(this.resolve, this.reject)
} // 儲存狀態,初始值是 pending
status = PENDING; // 成功之後的值
value = null; // 失敗之後的原因
reason = null; // 更改成功後的狀態
resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
} // 更改失敗後的狀態
reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
}
}

3、.then 的實現

then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value); // 呼叫成功回撥,並且把值返回
} else if (this.status === REJECTED) {
onRejected(this.reason); // 呼叫失敗回撥,並且把原因返回
}
}

上面三步已經簡單實現了一個 Promise我們先來呼叫試試:

const promise = new MyPromise((resolve, reject) => {
resolve('success')
reject('err')
}) promise.then(value => {
console.log('resolve', value)
}, reason => {
console.log('reject', reason)
}) // 列印結果:resolve success

經過測試發現如果使用非同步呼叫就會出現順序錯誤那麼我們怎麼解決呢?

4、實現非同步處理

// 定義三種狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'; class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
} // 成功存放的陣列
onResolvedCallbacks = [];
// 失敗存放法陣列
onRejectedCallbacks = [];
// 儲存狀態,初始值是 pending
status = PENDING;
// 成功之後的值
value = null;
// 失敗之後的原因
reason = null; // 更改成功後的狀態
resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onRejectedCallbacks.forEach((fn) => fn()); // 呼叫成功非同步回撥事件
}
} // 更改失敗後的狀態
reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallback.forEach((fn) => fn()); // 呼叫失敗非同步回撥事件
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value); //呼叫成功回撥,並且把值返回
} else if (this.status === REJECTED) {
onRejected(this.reason); // 呼叫失敗回撥,並且把原因返回
} else if (this.status === PENDING) {
// onFulfilled傳入到成功陣列
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value);
})
// onRejected傳入到失敗陣列
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
}
}
}

修改後通過呼叫非同步測試沒有

const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 2000);
}) promise.then(value => {
console.log('resolve', value)
}, reason => {
console.log('reject', reason)
}) // 等待 2s 輸出 resolve success

但是如果使用鏈式呼叫 .then 就會發現有問題,而原生的 Promise 是支援的。 那麼我們如何支援呢?

鏈式呼叫示例:

const promise = new MyPromise((resolve, reject) => {
resolve('success')
}) function other () {
return new MyPromise((resolve, reject) =>{
resolve('other')
})
}
promise.then(value => {
console.log(1)
console.log('resolve', value)
return other()
}).then(value => {
console.log(2)
console.log('resolve', value)
})
// 第二次 .then 將會失敗

5、實現 .then 鏈式呼叫

class MyPromise {
... then(onFulfilled, onRejected) {
// 為了鏈式呼叫這裡直接建立一個 MyPromise,並 return 出去
return new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
const x = onFulfilled(this.value);
resolvePromise(x, resolve, reject);
} else if (this.status === REJECTED) {
onRejected(this.reason);
} else if (this.status === PENDING) {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
})
}
} function resolvePromise(x, resolve, reject) {
// 判斷x是不是 MyPromise 例項物件
if (x instanceof MyPromise) {
// 執行 x,呼叫 then 方法,目的是將其狀態變為 fulfilled 或者 rejected
x.then(resolve, reject)
} else {
resolve(x)
}
}

6、也可以加入 try/catch 容錯

    ...
constructor(executor) {
try {
executor(this.resolve, this.reject)
} catch(error) {
this.reject(error)
}
}

最後

本文就先到這裡了,後續有時間再補充 .alL.any 等其他方法的實現。

相關推薦

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

作者:雨中愚

連結:https://juejin.cn/post/6995923016643248165

來源:掘金

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。