前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心,眾所周知哈, Promise
在我們們的開發中是相當的重要,我覺得對於 Promise
的使用等級,可以分為三個等級
- 1、掌握
Promise
的基本使用 - 2、掌握
Promise
的基本原理 - 3、在專案中能靈活運用
Promise
解決一些問題
第一點的話,其實就是能掌握 Promise
的一些基本使用方法以及一些方法,如 then、catch、all、race、finally、allSettled、any、resolve
等等
第二點的話,就是要能簡單實現一下 Promise
的原理,這能使我們對 Promise
的那些常用方法有更好的理解
第三點的話,就是要能靈活 Promise
解決我們們開發中的一些問題,今天我就給大家說一下我用 Promise
在專案開發中解決了什麼問題吧!
介面請求超時
顧名思義,就是給定一個時間,如果介面請求超過這個時間的話就報錯
1、自己實現
實現思路就是: 介面請求
和 延時函式
賽跑,並使用一個 Promise
包著,由於 Promise
的狀態是不可逆的,所以如果 介面請求
先跑完則說明 未超時
且 Promise
的狀態是 fulfilled
,反之, 延時函式
先跑完則說明 超時了
且 Promise
的狀態是 rejetced
,最後根據 Promise
的狀態來判斷有無超時
/**
* 模擬延時
* @param {number} delay 延遲時間
* @returns {Promise<any>}
*/
function sleep(delay) {
return new Promise((_, reject) => {
setTimeout(() => reject('超時嘍'), delay)
})
}
/**
* 模擬請求
*/
function request() {
// 假設請求需要 1s
return new Promise(resolve => {
setTimeout(() => resolve('成功嘍'), 1000)
})
}
/**
* 判斷是否超時
* @param {() => Promise<any>} requestFn 請求函式
* @param {number} delay 延遲時長
* @returns {Promise<any>}
*/
function timeoutPromise(requestFn, delay) {
return new Promise((resolve, reject) => {
const promises = [requestFn(), sleep(delay)]
for (const promise of promises) {
// 超時則執行失敗,不超時則執行成功
promise.then(res => resolve(res), err => reject(err))
}
})
}
2、Promise.race
其實 timeoutPromise
中的程式碼可以使用 Promise.race
來代替,是同樣的效果
function timeoutPromise(requestFn, delay) {
// 如果先返回的是延遲Promise則說明超時了
return Promise.race([requestFn(), sleep(delay)])
}
3、測試
// 超時
timeoutPromise(request, 500).catch(err => console.log(err)) // 超時嘍
// 不超時
timeoutPromise(request, 2000).then(res => console.log(res)) // 成功嘍
轉盤抽獎
我們平時在轉盤抽獎時,一般都是開始轉動的同時也發起介面請求,所以有兩種可能
- 1、轉盤轉完,介面還沒請求回來,這是不正常的
- 2、轉盤轉完前,介面就請求完畢,這是正常的,但是需要保證
請求回撥
跟轉盤轉完回撥
同時執行
1、轉盤轉完,介面還沒請求回來
主要問題就是,怎麼判斷 介面請求時間
是否超過 轉盤轉完所需時間
,我們們其實可以用到上一個知識點 介面請求超時
,都是一樣的道理。如果 轉盤轉完所需時間
是 2500ms
,那我們們可以限定 介面請求
需要提前 1000ms
請求回來,也就是 介面請求
的超時時間為 2500ms - 1000ms = 1500ms
/**
* 模擬延時
* @param {number} delay 延遲時間
* @returns {Promise<any>}
*/
function sleep(delay) {
return new Promise((_, reject) => {
setTimeout(() => reject('超時嘍'), delay)
})
}
/**
* 模擬請求
*/
function request() {
return new Promise(resolve => {
setTimeout(() => resolve('成功嘍'), 1000)
})
}
/**
* 判斷是否超時
* @param {() => Promise<any>} requestFn 請求函式
* @param {number} delay 延遲時長
* @returns {Promise<any>}
*/
function timeoutPromise(requestFn, delay) {
return Promise.race([requestFn(), sleep(delay)])
}
2、轉盤轉完前,介面就請求完畢
我們們確保了 介面請求
可以在 轉盤轉完
之前請求回來,但是還有一個問題,就是需要保證 請求回撥
跟 轉盤轉完回撥
同時執行,因為雖然 介面請求
請求回來的時候,轉盤還在轉著,我們們需要等轉盤轉完時,再一起執行這兩個回撥
聽到這個描述,相信很多同學就會想到 Promise.all
這個方法
// ...上面程式碼
/**
* 模擬轉盤旋轉到停止的延時
* @param {number} delay 延遲時間
* @returns {Promise<any>}
*/
function turntableSleep(delay) {
return new Promise(resolve => {
setTimeout(() => resolve('停止轉動嘍'), delay)
})
}
/**
* 判斷是否超時
* @param {() => Promise<any>} requestFn 請求函式
* @param {number} turntableDelay 轉盤轉多久
* @param {number} delay 請求超時時長
* @returns {Promise<any>}
*/
function zhuanpanPromise(requsetFn, turntableDelay, delay) {
return Promise.all([timeoutPromise(requsetFn, delay), turntableSleep(turntableDelay)])
}
3、測試
// 不超時,且先於轉盤停止前請求回資料
zhuanpanPromise(request, 2500, 1500).then(res => console.log(res), err => console.log(err))
控制併發的Promise的排程器
想象一下,有一天你突然一次性發了10個請求,但是這樣的話併發量是很大的,能不能控制一下,就是一次只發2個請求,某一個請求完了,就讓第3個補上,又請求完了,讓第4個補上,以此類推,讓最高併發量變成可控的
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
的輸出順序是:2 3 1 4
整個的完整執行流程:
一開始1、2兩個任務開始執行
500ms時,2任務執行完畢,輸出2,任務3開始執行
800ms時,3任務執行完畢,輸出3,任務4開始執行
1000ms時,1任務執行完畢,輸出1,此時只剩下4任務在執行
1200ms時,4任務執行完畢,輸出4
實現
class Scheduler {
constructor(limit) {
this.queue = []
this.limit = limit
this.count = 0
}
add(time, order) {
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(order)
resolve()
}, time)
})
}
this.queue.push(promiseCreator)
}
taskStart() {
for(let i = 0; i < this.limit; i++) {
this.request()
}
}
request() {
if (!this.queue.length || this.count >= this.limit) return
this.count++
this.queue.shift()().then(() => {
this.count--
this.request()
})
}
}
測試
// 測試
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
取消重複請求
舉個例子,我們們在做表單提交時,為了防止多次重複的提交,肯定會給按鈕的點選事件加上 防抖措施
,這確實是有效地避免了多次點選造成的重複請求,但是其實還是有弊端的
眾所周知,為了使用者更好地體驗, 防抖
的延時是不能太長的,一般在我的專案中都是 300ms
,但是這隻能管到 請求時間 < 300ms
的介面請求,如果有一個介面請求需要 2000ms
,那麼此時 防抖
也做不到完全限制 重複請求
,所以我們們需要額外做一下 取消重複請求
的處理
實現
實現思路:簡單說就是,利用 Promise.race
方法,給每一次請求的身邊安裝一顆雷,如果第一次請求後,又接了第二次重複請求,那麼就執行第一次請求身邊的雷,把第一次請求給炸掉,以此類推。
class CancelablePromise {
constructor() {
this.pendingPromise = null
this.reject = null
}
request(requestFn) {
if (this.pendingPromise) {
this.cancel('取消重複請求')
}
const promise = new Promise((_, reject) => (this.reject = reject))
this.pendingPromise = Promise.race([requestFn(), promise])
return this.pendingPromise
}
cancel(reason) {
this.reject(reason)
this.pendingPromise = null
}
}
function request(delay) {
return () =>
new Promise(resolve => {
setTimeout(() => {
resolve('最後贏家是我')
}, delay)
})
}
測試
const cancelPromise = new CancelablePromise()
// 模擬頻繁請求5次
for (let i = 0; i < 5; i++) {
cancelPromise
.request(request(2000))
.then((res) => console.log(res)) // 最後一個 最後贏家是我
.catch((err) => console.error(err)); // 前四個 取消重複請求
}
全域性請求loading
比如一個頁面中,或者多個元件中都需要請求並且展示 loading狀態
,此時我們不想要每個頁面或者元件都寫一遍 loading
,那我們可以統一管理 loading
, loading
有兩種情況
- 1、全域性只要有一個介面還在請求中,就展示
loading
- 2、全域性所有介面都不在請求中,就隱藏
loading
那我們怎麼才能知道全域性介面的請求狀態呢?其實我們們可以利用 Promise
,只要某個 介面請求Promise
的狀態不是 pending
那就說明他請求完成了,無論請求成功或者失敗,既然是無論成功失敗,那我們們就會想到 Promise.prototype.finally
這個方法
實現
class PromiseManager {
constructor() {
this.pendingPromise = new Set()
this.loading = false
}
generateKey() {
return `${new Date().getTime()}-${parseInt(Math.random() * 1000)}`
}
push(...requestFns) {
for (const requestFn of requestFns) {
const key = this.generateKey()
this.pendingPromise.add(key)
requestFn().finally(() => {
this.pendingPromise.delete(key)
this.loading = this.pendingPromise.size !== 0
})
}
}
}
測試
// 模擬請求
function request(delay) {
return () => {
return new Promise(resolve => {
setTimeout(() => resolve('成功嘍'), delay)
})
}
}
const manager = new PromiseManager()
manager.push(request(1000), request(2000), request(800), request(2000), request(1500))
const timer = setInterval(() => {
// 輪詢檢視loading狀態
console.log(manager.loading)
}, 300)
參考
結語
如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群,我們一起好好學習啊啊啊啊啊啊啊,我會定時模擬面試,簡歷指導,答疑解惑,我們們互相學習共同進步!!