作爲前端開發人員,對於promise應該都是不陌生,基本上都有過,new promise, 然後.then, 原型裏也提供了一些方案,race, all等等。。。對於解決同步的問題可謂是十分方便。然而,前段時間面試:
“知道promise嗎”,
“知道”,
“那你說說什麼是promise”,
“promise就是解決異步操作的一種方案,避免的函數回調”,
“嗯,那還有呢,具體怎麼實現的呢”
“emmmm…”
“ok,那你知道async麼,這個和promise區別是什麼呢?”
…真的是一時語塞,當時緊張的隨便說了兩句,現在都忘了自己說了些什麼。原來平時真的是專注於怎麼方便怎麼來,本着會用就行了的心態果然很容易把自己的路走窄呀。
所以,還是乖乖關注其方法本身吧。
#####爲什麼要用promise呢?
答案很明顯,promise是爲了解決異步的可操控性。node.js是單線程異步執行環境的。如果兩異步接口存在依賴關係,比如A接口需要依賴B接口,那麼我們希望A接口在B接口之前執行。
我們也許用如下的方式去控制方法的執行順序
B接口裏需要執行ajax請求,在B方法中定義一個變量val來監聽ajax請求是否返回正確的值,再通過val來判斷是否需要調用A方法。(以下代碼爲簡寫,理順思路就好)
function A(){
console.log('----輸出A----')
}
function B(val){
if (val < 5) {
A();
}
}
B(4);
或者是通過setTimeOut來控制方法的執順序。
function A(){
console.log('----輸出A----')
}
function B(val){
console.log('---val----'+val);
setTimeout(()=>{
A();
},5000)
}
B(4);
通過函數回調的方式如果是非常簡單的操作,這樣的寫法也未嘗不可。
但是如果是很複雜的邏輯,也許過了十天自己都回憶不起來,這是個啥。。
另外,這樣寫的壞處,就真的是造成了代碼冗餘,而且不能複用,並且讓你覺得寫代碼是件繁瑣的事情。
函數回調,很可能遇見信任問題,what?
1.調用回調過早(在它開始追蹤之前)
2.調用回調過晚(或者不調)
3.調用回調太少或者太多次
4.吞掉了可能發生的錯誤/異常
5.沒有傳遞正確的參數(還是有可能的,例如接口相應超時,超出了等待時間)
…
總之使用回調的方式,確實會出現很多讓人難受又很奇葩的錯誤。
#####首先會用。ok,會用的程度是什麼樣子的呢?
new Promise(function(resolve, reject){
console.log('---執行步驟一,輸出,成功---')
resolve();
}).then(function(resolve,reject){
console.log('---執行步驟二,輸出,失敗----')
reject();
}).catch(function(resolve, reject){
console.log('----最後一步失敗---')
})
輸出
—執行步驟一,輸出,成功—
—執行步驟二,輸出,失敗----
----最後一步失敗—
甚至,我們在vue/react中,使用axios,fetch,dispatch等請求數據也會去支持promise API,非常直觀的表達了函數的調用順序。比如說:
dispatch({
type: 'xxx/xx',
params:{
content: 'test1'
}
}).then(res => {
if (res) {
...doSomeThing...
}
})
以上,我想大部分都用到這個程度吧。
#####來剖析剖析promise
如果你來實現一個promise函數呢?應該怎麼寫?我們沿着這個思路,寫一個promise的實現方案,順便來了解了解它的思想。
我們以生活中的例子:
比如有一天我們需要外出辦一件很緊急的事情,但是朋友又要來家裏聚餐。最理想的狀態就是,當我辦完緊急的事情回來後,可以和朋友們聚在一起,吃着美食。那麼promise在這時候充當的角色就是我的小助手mm,她需要爲我做好一頓美食。接下來,我需要列好任務,告訴她如何才能做好這頓美食。以下就是我的任務清單。
1.去菜市場買菜
2.將買回來的菜熬成湯,做成好吃的東西
3.打電話告訴我的朋友通知他們過來聚餐
4.通知我回家
1,2,3步驟我們就理解爲異步的執行過程吧,但是我要告訴mm我希望的過程。那麼將上面的話用promise的寫法,就可以翻譯成
// 告訴mm幫我做幾件連貫的事情,先去菜市場買菜
new Promise(買菜)
//用買好的菜做飯
.then((買好的菜)=>{
return new Promise(做飯);
})
//打電話告訴朋友們飯好了
.then((做好的飯)=>{
return new Promise(飯好了);
})
//通知完朋友打電話通知我
.then((通知朋友玩了)=>{
電話通知我();
})
promise是承若我一定幫你做一件事情,並且會告訴你事情的結果。
那麼promise的核心還在於它的狀態機制,一共有三個狀態。
pending: 異步正在進行中
resolved: 執行成功
reject: 執行失敗
那麼按照狀態,則是pendding => resolved => 返回結果
=> reject => 返回錯誤結果
也就是說,執行成功後,pedding狀態變爲resolved,執行失敗後,pedding狀態變爲reject。這個狀態一旦發生改變了,就不會再變回去了。
#####那就開始實現吧
這裏就簡單說說思路,具體時間這裏整理時間也沒有那麼多了。
promise裏面有then,resolve,reject這兩個靜態方法,也提供了all,race這些方法。
開始的第一步:
初始化promise實例,需要達到的目的:
1.改變promise中的status狀態,將pedding狀態變爲resolved或者rejected。
2.當檢測到是成功還是失敗的狀態時,返回相應的執行函數,傳遞出promise執行對象
(export default) class PromistMine{
constructor(executor){
// 狀態,promise裏面基本上就這種狀態了
this.status = 'pedding';
this.value = undefined; // 返回失敗的值
this.reason = undefined; // 失敗成功原因
this.onRejectCallBacks = []; // 存放失敗的回調,這個是成功的回調函數
this.onResolveCallBacks = []; // 存放成功的回調,這個是失敗的回調函數
/**
* 處理的兩種狀態
**/
// 執行成功的狀態
let resolve = (reason) => {
if (this.status === 'pedding') {
this.status = 'resolved';
// 把成功後要返回的信息透傳
this.reason = reason;
this.onRejectCallBacks(fn => fn());
}
}
// 執行失敗的狀態
let reject = (data) => {
if (this.status === 'pedding') {
this.status = 'rejected';
this.value = data;
this.onRejectCallBacks(fn => fn())
}
}
// 在new Promise時,都需要傳遞兩個參數,resolve,reject用於處理方法是否成功後的回調
try {
executor(resolve,reject);
} catch (e) {
reject(e) // 執行執行器,如果throw new Error('error') 就直接走reject了
}
}
}
開始的第二步:
實現resolve和reject這兩個靜態方法。
resolve和reject這兩個方法本質上其實是一樣的。這裏是promise提供的兩個靜態方法,目的在於,狀態是成功或者失敗時,返回自身。
這兩個靜態方法其實和構造函數裏的resolve與reject原理一樣。
開始的第三步:
實現then的鏈式調用。
在平時使用promise,then字面意思是“下一步,接下來”。這個回調函數裏,可以做的事情是:1.執行上一步的方法,並且在上一步的返回結果裏執行一系列的操作。2.返回自身實例,記錄執行結果,便於鏈式調用。
在原型上添加方法,new的時候直接繼承了
PromistMine.prototype.then((onFulfilled, onRejected) => {
// 因爲從原型上新增的方法,所以可以拿到this作用域
if (this.status === 'pendding') {
// 將報錯或者成功的信息存起來
this.onRejectCallBacks = this.onRejectCallBacks.push(onRejected);
this.onResolveCallBacks = this.onResolveCallBacks.push(onFulfilled)
} else if (this.status === FULFILLED){
onFulfilled(value);
} else {
onRejected(this.reason);
}
return this; // 返回其本身
})
大概的思路就是這些了。能看到這已經很不容易了。
記錄一下常用race,all的使用
先說說all吧。
promise的all方法,在官方的類型是這樣去描述這個方法的。它允許多個promise對象以數組的形式傳入。
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
promise提供的all方法,有一種共存亡的概念。當數組內所有的promise都被接受了,那麼執行成功的回調;如果傳入的promise數組中有一個被拒絕了,那麼就執行失敗後的回調;如果存在多個promise被拒絕了,返回的信息將是第一個promise被拒絕的消息。
for example,其他的狀況就不一一列舉了:
const demo1 = new Promise((resolve, reject)=>{
reject('接受到')
});
const demo2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('接收到了')
},122)
})
Promise.all([demo1,demo2]).then(resolve=>{
console.log(resolve)
}, reject=>{
console.log(reject)
})
output: 接受到
promise提供的race方法,race按表面的意思可以知道是賽跑的意思。race的入參和all方法是一樣。允許傳入多個promise對象。但是race更追求的是速度,不管是接受狀態還是拒絕狀態,誰快誰先輸出。
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 110, 'one');
});
var promise2 = new Promise(function(resolve, reject) {
setTimeout(reject, 100, 'two');
});
Promise.race([promise1, promise2]).then(function(value) {
console.log(value);
// Both resolve, but promise2 is faster
});
在前面提到,在promise中,有三種狀態,pending,resolved,rejected。但在pedding變爲resolved或者rejected時,這個狀態我們叫決議狀態。
在靜態方法reject,resolve這兩個方法,可以改變返回的狀態,reject,resolve不會直接傳參,而是將參數當做拒絕或者接受原因拒絕promise。
在resolve方法中,傳入一個reject,此時返回的也是一個reject,被拒絕的promise。
例如
new Promise((resolve, reject)=>{
resolve(Promise.reject('---解決了---'));
}).then((resolve)=>{
console.log('----成功解決---');
console.log(resolve)
},(reject)=>{
console.log('-----失敗解決---');
console.log(reject);
})
> -----失敗解決---
> ---解決了---
記錄一下promise的then
promise提供了then的方法,方便函數的回調。而前面我們也提到,then它自身的意義在於記錄執行結果,返回其本身,這樣就可實現鏈式調用了。
promise在平時用到最多的場景,調用接口後再執行其他的操作。在我剛剛接觸promise時,總會寫出讓人驚奇的代碼,找錯誤找了半天,最主要的就是平時對promise的理解還不夠。
讓人驚奇代碼之一:
new Promise((resolve,reject)=>{
console.log('---執行步驟1---');
}).then(()=>{
console.log('---執行步驟2---');
}).then(()=>{
console.log('---執行步驟3---');
})
---執行步驟1---
這裏的promise並沒有執行resolve,所以promise一直在一個決議的狀態,也沒有返回其自身的實例,所以then裏面的方法不執行那是當然的。
讓人驚奇代碼之二:
const promise = new Promise((resolve, reject) => {
resolve('完成');
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
> first messaeg: 完成
> second messaeg: undefined
這裏一直取不到值,又是爲什麼呢?
悄悄的改一下代碼
const promise = new Promise((resolve, reject) => {
resolve('完成');
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
return 'new'+msg;
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
> first messaeg: 完成
> second messaeg: new完成
爲什麼呢?因爲第二個then裏,接受上一個return的返回值,也就是新的promise是根據上一個函數的返回來進行決議,但是上一個then並沒有return值,默認值爲undefined,所以第一段代碼會return一個undefined。