前言
Promise 作為一個前端必備技能,不管是從項目應用還是面試,都應該對其有所了解與使用。
常常遇到的面試五連問:
- 說說你對 Promise 理解?
- Promise 的出現解決了什么問題?
- Promise 有哪些狀態?
- 你用過 Promise 哪些方法?
- 如何實現一個 Promise ?
什么是 Promise?
Promise 是異步編程的一種解決方案:從語法上講,promise 是一個對象,從它可以獲取異步操作的消息;從本意上講,它是承諾,承諾它過一段時間會給你一個結果。
Promise 有三種狀態:pending(等待態),fulfiled(成功態),rejected(失敗態);狀態一旦改變,就不會再變。創造 Promise 實例后,它會立即執行。
一般來說我們會碰到的回調嵌套都不會很多,一般就一到兩級,但是某些情況下,回調嵌套很多時,代碼就會非常繁瑣,會給我們的編程帶來很多的麻煩,這種情況俗稱——回調地獄。
這時候我們的Promise 就應運而生、粉墨登場了。
Promise 的基本使用
Promise 是一個構造函數,自己身上有 all
、reject
、resolve
這幾個眼熟的方法,原型上有 then
、catch
等同樣很眼熟的方法。
首先創建一個 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
是否成功完成后都需要執行的代碼提供了一種方式。
這避免了同樣的語句需要在 then
和 catch
中各寫一次的情況。
// 加載 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 都已經fulfilled
或rejected
后的 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
都失敗/拒絕),就返回一個失敗的 promise
和 AggregateError
類型的實例,它是 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
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。