由專案需求中引出的思考,Promise鏈式呼叫如何防抖
大致需求是:有一個表格,點選其中任意一行會載入一些與之相關的詳細內容。載入這個步驟是一個Promise鏈,會依次從2個不同的伺服器端獲取相關資訊(存在依賴關係無法同時傳送請求)。
在短時間內多次點選時,由於載入的時間每次不一樣,可能會造成最終顯示的不是最後一次點選的內容,且每一次點選都會有DOM操作從而造成瀏覽器效能的損失。到這裡,我們很自然的想到了利用防抖來進行延遲執行。但問題來了,載入的時間是個很大的區間(幾百毫秒到幾秒都有可能),傳統的防抖在這個情況下並不適用。
舉個例子,我們延遲500毫秒執行,第一次點選載入花了2秒,1秒後我們又點了一次載入,這次只花了500毫秒,結果就是最終先顯示後一次結果,然後被前一次結果覆蓋。如果我們設定一個過大的延遲值,那將會極大的降低使用者體驗。
當然我們也可以限制使用者點選:即第一次點選結果出來前無法再次點選。但這個方案被客戶直接否決了o(一︿一+)o
由此引出今天討論的話題,如何實現當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 複製程式碼
第一次發文,不足之處還請輕噴,歡迎指出錯誤,如果你有更好的方法,也希望大家一起共同探討共同進步~