你說你會Promise?那你解決一下專案中的這五個難題?

Sunshine_Lin發表於2021-12-02

前言

大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心,眾所周知哈, 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 的狀態來判斷有無超時

截圖2021-11-22 下午9.58.49.png

/**
 * 模擬延時
 * @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)

參考

結語

如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群,我們一起好好學習啊啊啊啊啊啊啊,我會定時模擬面試,簡歷指導,答疑解惑,我們們互相學習共同進步!!
截圖2021-11-28 上午9.43.19.png

相關文章