你知道嗎?大部分人對于 Promise 的開發程度遠遠不足 10%!
大家都知道 Promise 是 JavaScript 的一個非常強大的特性,但是很多人對于 Promise 的認識還停留在 網絡請求上 ,而網絡請求大部分人都用 axios,axios 已經幫我們封裝好了,所以自然而然地,大部分人在項目中并沒有過多使用過 Promise 這個強大的特性。
接下來總結一下我在項目中用到的 Promise 的場景。
打開彈窗的事件回調
如下場景,因為邏輯比較多,我封裝了一個 useModal 的 Hook,并在頁面中去調用,打開彈窗,打開彈窗的時候,接收兩個回調:
- onSuccess:成功的回調
- onError:失敗的回調
因為我需要在頁面調用的時候,成功和失敗之后都需要在頁面中做一些額外的操作。
圖片
接著我在頁面中去調用這個 Hook,并使用了 open 方法,傳入兩個回調。
圖片
不過后來我想了一下,如果我對于這個 Modal,只在乎 成功回調 和 失敗回調 的話,那可不可以直接 鏈式調用就行了呢?,就類似于下面這樣,有點類似于那些組件庫中的 form.validate 方法,也是返回一個 Promise。
圖片
那么就得借助 Promise 的力量了。
圖片
充當發布訂閱
說到發布訂閱,大家就想到了 EventBus、EventEmit 這些具備 emit、on 的元素,但是有時候我們所需要的 發布訂閱 并不需要這么復雜。
我們需要的可能只是,頁面1 完成了某件事情后,通知一下 頁面2,僅此而已。
我們需要先封裝一個 onReadyFn 的工具函數,這函數返回兩個東西。
- readyResolve: 一個resolve函數
- onReady: 接收回調函數,只有在readyResolve執行后才會執行
圖片
接著封裝一個 usePage1.ts,用來處理 Page1 的邏輯,并在處理完邏輯后發布通知。
圖片
圖片
現在通知有了,只需要訂閱即可。
圖片
可以到控制臺里,看到了這個運行順序是符合我們的預期的
圖片
檢測接口請求超時
我們使用 axios 的時候可以設置 timeout 去控制接口的超時時間,但是有的項目并沒有用 axios 那咋辦呢?那就得自己去檢測接口請求超時啦。
大概思路是這樣的,比如你設置的超時時長是 2000ms。
- 讓實際請求跟一個 2000ms 的定時器去賽跑
- 定時器先執行完,則說明實際請求超時了
- 實際請求先執行完,則說明沒有超時
說到賽跑,咱們第一時間可以想到 Promise.race,是的,就是用它來檢測接口超時。
圖片
圖片
控制多并發
提起控制并發,大家應該不陌生,我們可以先來看看多并發,再去聊聊為什么要去控制它。
多并發一般是指多個異步操作同時進行,而運行的環境中資源是有限的,短時間內過多的并發,會對所運行的環境造成很大的壓力,比如前端的瀏覽器,后端的服務器,常見的多并發操作有:
- 前端的多個接口同時請求
- 前端多條數據異步處理
- Nodejs的多個數據操作同時進行
- Nodejs對多個文件同時進行修改
圖片
正是因為多并發會造成壓力,所以我們才需要去控制他,降低這個壓力~,比如我可以控制最大并發數是 3,這樣的話即使有100個并發,我也能保證最多同時并發的最大數量是 3。
圖片
大致實現思路就是,假設現在有 9 個并發,我設置最大并發為 3,那么我將會走下面這些步驟:
- 1、先定好三個坑位
- 2、讓前三個并發進去坑位執行
- 3、看哪個坑位并發先執行完,就從剩余的并發中拿一個進去補坑
- 4、一直重復第 3 步,一直到所有并發執行完
圖片
在進行多并發的時候,我們通常會使用 Promise.all ,但是 Promise.all并不能控制并發,或者說它本來就沒這個能力,我們可以看下面的例子。
圖片
最后是同時輸出,這說明這幾個并發是同時發生的。
圖片
期望效果
圖片
圖片
實現 limitFn
我們需要在函數內部維護兩個變量:
- queue:隊列,用來存每一個改造過的并發
- activeCount:用來記錄正在執行的并發數
并聲明函數 generator ,這個函數返回一個 Promise,因為 Promise.all 最好是接收一個 Promise 數組。
圖片
接下來我們來實現 enqueue 這個函數做兩件事:
- 將每一個 fetchFn 放進隊列里
- 將坑位里的 fetchFn 先執行
圖片
假如我設置最大并發數為 2,那么這一段代碼在一開始的時候只會執行 2 次,因為一開始只會有 2 次符合 if 判斷,大家可以思考一下為什么~
圖片
一開始執行 2 次,說明這時候兩個坑位已經各自有一個 fetchFn 在執行了。
接下來我們實現 run 函數,這個函數是用來包裝 fetch 的,他完成幾件事情:
1、將 activeCount++ ,這時候執行中的并發數 +1
2、將 fetchFn 執行,并把結果 resolve 出去,說明這個并發執行完了
3、將 activeCount--,這時候執行中的并發數 -1
4、從 queue 中取一個并發,拿來補坑執行
圖片
其實第 3、4 步,是在 next 函數里面執行的
圖片
代碼
充當發布訂閱
// hooks/onReadyFn
export const onReadyFn = () => {
let readyResolve: any = null;
const readyPromise = new Promise(resolve => {
// 把 resolve 保存起來
readyResolve = resolve;
});
const onReady = (cb: (...args: any[]) => void) => {
readyPromise.then((...args: any[]) => {
// resolve 執行完才會走 then
// 才會執行回調函數
cb(...args);
});
};
return {
onReady,
readyResolve,
};
};
接口超時
// 模擬實際請求
const fetchFn = (delay: number) => {
return new Promise<number>(resolve => {
setTimeout(() => {
resolve(1);
}, delay);
});
};
// 判斷是否超時
const checkTimeout = (delay: number, timeout: number) => {
return Promise.race([
fetchFn(delay),
new Promise((_, reject) => {
setTimeout(() => {
reject('超時啦!');
}, timeout);
}),
]);
};
// 沒超時
checkTimeout(100, 500)
.then(res => {
console.log('沒超時', res);
})
.catch(err => {
console.log(err);
});
// 超時了
checkTimeout(600, 500)
.then(res => {
console.log('沒超時', res);
})
.catch(err => {
console.log(err);
});
控制并發
const limitFn = (limit) => {
const queue = [];
let activeCount = 0;
const next = () => {
activeCount--;
if (queue.length > 0) {
queue.shift()();
}
};
const run = async (fn, resolve, ...args) => {
activeCount++;
const result = (async () => fn(...args))();
try {
const res = await result;
resolve(res);
} catch { }
next();
};
const enqueue = (fn, resolve, ...args) => {
queue.push(run.bind(null, fn, resolve, ...args));
if (activeCount < limit && queue.length > 0) {
queue.shift()();
}
};
const generator = (fn, ...args) =>
new Promise((resolve) => {
enqueue(fn, resolve, ...args);
});
return generator;
};