由專案需求中引出的思考,Promise鏈式呼叫如何防抖

Yukan發表於2019-03-17

03/18 - 20:36 更新了一些的更具體的問題描述

想和大家探討一下最近專案上遇到的一個防抖問題。

問題概述

大致需求是:有一個表格,點選其中任意一行會載入一些與之相關的詳細內容(與表格在同一頁面)。載入這個步驟是一個Promise鏈,會依次從2個不同的伺服器端獲取相關資訊(存在依賴關係無法同時傳送請求)。

在短時間內多次點選時,由於載入的時間每次不一樣,可能會造成最終顯示的不是最後一次點選的內容,且每一次點選都會有DOM操作從而造成瀏覽器效能的損失。

我們認為最合理的當然是載入過程中阻止使用者繼續點選,然而此方案被客戶否決了:使用者不應該被限制自由,假如使用者點錯了,還要等載入完才能改嗎等等o(一︿一+)o

到這裡,我們很自然的想到了利用防抖來進行延遲執行。但問題來了,載入的時間是個很大的區間(幾百毫秒到幾秒都有可能),傳統的防抖在這個情況下並不適用。

舉個例子,我們延遲500毫秒執行,第一次點選載入花了2秒,1秒後我們又點了一次載入,這次只花了500毫秒,結果就是最終先顯示後一次結果,然後被前一次結果覆蓋。如果我們設定一個過大的延遲值,那將會極大的降低使用者體驗。

由專案需求中引出的思考,Promise鏈式呼叫如何防抖

由此引出今天討論的話題,如何實現當Promise鏈未獲取最終結果前,只有最後一次點選能夠操作DOM改變頁面。
P.S.由於實際工程比較複雜,http請求被封裝在其他的模組中,所以在這裡不考慮通過abort來終止請求以達到更好的優化。

以下為實際問題簡化版:p1、p2、p3形成Promise鏈,可以看到,每次點選都會執行改變頁面。(固定了Promise執行時間,且多加了一個Promise來更好的擴充套件假設有n個Promise的情況)

const p1 = (data) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(data + 1), 200);
  });
};
const p2 = (data) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(data + 2), 300);
  });
};
const p3 = (data) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(data + 3), 500);
  });
};
const onClick = (data) => {
  p1(data)
    .then(data => p2(data))
    .then(data => p3(data))
    .then(result => {
      // 實際情況為操作返回值改變頁面
      console.log(result);
    })
    .catch(err => {
      // 處理錯誤
    });
};
// 模擬點選
onClick(1);
setTimeout(() => onClick(2), 400);
setTimeout(() => onClick(3), 2000);
// 7
// 8
// 9
複製程式碼

方案一

我們可以在onClick上設定一個counter,每次點選加1,只有當前值匹配counter時才改變頁面。

// 省略p1,p2,p3申明
let counter = 0;
const onClick = (data) => {
  const current = ++counter;
  p1(data)
    .then(data => p2(data))
    .then(data => p3(data))
    .then(result => {
      if (current === counter) {
        // 實際情況為操作返回值改變頁面
        console.log(result);
      }
    })
    .catch(err => {
      if (current === counter) {
        // 處理錯誤
      }
    });
};
onClick(1);
setTimeout(() => onClick(2), 400);
setTimeout(() => onClick(3), 2000);
// 第一個onClick不會重新整理頁面
// 8
// 9 第三個點選時第二個已經重新整理,所以第三個繼續重新整理頁面
複製程式碼

這個方案解基本解決了問題,但是仔細想想,實際上在每次點選時,所有的Promise鏈還是完全都執行了。 比如在第二個onClick時,第一個的Promise鏈才執行到p2,那麼能不能不執行p3來達到更好的優化呢?

方案二:在方案一的基礎上進一步優化

通過在每個Promise上巢狀一個函式來實現進一步優化,如果不匹配counter,直接reject中斷Promise鏈。

// 省略p1,p2,p3申明
let counter = 0;
const onClick = (data) => {
  const current = ++counter;
  p1(data)
    .then(wrapWithCancel(p2))
    .then(wrapWithCancel(p3))
    .then(result => {
      if (current === counter) {
        // 實際情況為操作返回值重新整理頁面
        console.log(result);
      }
    })
    .catch(err => {
      if (current === counter && err !== 'cancelled') {
        // 處理除了cancelled以外的錯誤
      }
    });

  function wrapWithCancel(fn) {
    return (data) => {
      if (current === counter) {
        return fn(data);
      } else {
        return Promise.reject('cancelled');
      }
    }
  }
};
onClick(1);
setTimeout(() => onClick(2), 100);
setTimeout(() => onClick(3), 400);
// 第一個onClick的p2和p3都不會執行
// 第二個onClick的p3不會執行
// 9
複製程式碼

方案三:加上常規的防抖延遲執行

我們同樣可以在這基礎上加上常規的防抖延遲執行,進一步優化:

// 省略p1,p2,p3申明
let counter = 0;
const onClick = (data) => {
  const current = ++counter;
  p1(data)
    .then(wrapWithCancel(p2))
    .then(wrapWithCancel(p3))
    .then(result => {
      if (current === counter) {
        // 實際情況為操作返回值重新整理頁面
        console.log(result);
      }
    })
    .catch(err => {
      if (current === counter && err !== 'cancelled') {
        // 處理除了cancelled以外的錯誤
      }
    });

  function wrapWithCancel(fn) {
    return (data) => {
      if (current === counter) {
        return fn(data);
      } else {
        return Promise.reject('cancelled');
      }
    }
  }
};
const debounce = function (fn, wait) {
  var timer = null;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  }
};
const debounced = debounce(onClick, 200);
debounced(1);
setTimeout(() => debounced(2), 100);
setTimeout(() => debounced(3), 200);
setTimeout(() => debounced(4), 600);
// 前兩個onClick的p1,p2和p3都不會執行
// 第三個onClick的p3不會執行
// 10 
複製程式碼

第一次發文,不足之處還請輕噴,歡迎指出錯誤,如果你有更好的方法,也希望大家一起共同探討共同進步~

相關文章