淺談可取消的Promise

最近再嘗試實現CSSTransition組件,其中有個需求是每次in屬性的變化會致使className的變化,爲了增長對應的效果,必須保證不一樣的className在一段時間範圍內,按照特順序進行顯示。promise

我嘗試使用promise來控制順序,效果很不錯,幾乎解決了問題,可是,因爲用戶行爲的不肯定性,好比瘋狂點擊toggle按鈕,in屬性的可能在短期內大量變化,從而觸發大量的回調函數,使得className的變化順序變得至關混亂。網絡

因而乎,天然而然的,每次觸發回調前,要先取消掉先前可能存在的回調函數,這個需求有點像多tab觸發網絡請求的常見,每次點擊tab都會觸發一個網絡請求,爲了避免讓界面顯示老舊的數據(由於異步的緣由,新舊請求返回數據的時間順序是不肯定的),必須取消掉先前的請求。異步

此外,另外一個難點在於鏈式的回調調用,你必須保證在取消後,不論回調鏈執行到哪裏,都不會再被執行了。async

  • 沒考慮取消的版本
if (isIn) { // 每次點擊,判斷變化的isIn屬性,觸發相應的回調
    setClassName(`${initClassNameRef.current} enter`)
      .then(() => setClassName(`${initClassNameRef.current} enter enter-active`))
      .then(() => wait(timeout))
      .then(() => setClassName(`${initClassNameRef.current} enter-done`));
    } else {
    setClassName(`${initClassNameRef.current} exit`)
      .then(() => setClassName(`${initClassNameRef.current} exit exit-active`))
      .then(() => wait(timeout))
      .then(() => setClassName(`${initClassNameRef.current} enter-done`));
    }
複製代碼

問題很明顯,一旦用戶屢次點擊按鈕(很是可能),className值變化順序就不肯定了。函數

在查看了網上各類取消Promise的方案後,我放棄了引入polyfill庫的方案,還有一些方案看着很不直觀,必須在徹底瞭解js代碼執行順序的狀況下,才能明白爲何這樣是能夠取消的。最後,我嘗試實現了一個簡單,易於理解的版本。post

type PormiseMaker = (prevPromiseValue: any) => Promise<any>;

interface ICancelToken {
  cancel: () => void;
  finally: (callback: () => void) => void;
}

function cancelablePromiseChain(...promiseMakers: Array<(prevValue: any) => Promise<any>>): ICancelToken {
  let isCanceled = false;
  let finallyCallback: undefined | (() => void);
  const runner = (async function runner() {
    let prevResult;
    if (isCanceled) {
      if (typeof finallyCallback === 'function') finallyCallback();
      return;
    }
    for (const promiseMaker of promiseMakers) {
      if (isCanceled) {
        if (typeof finallyCallback === 'function') finallyCallback();
        return;
      }
      prevResult = await promiseMaker(prevResult);
    }
  }());
  return {
    cancel() {
      isCanceled = true;
    },
    finally(callback) {
      finallyCallback = callback;
    }
  }
}
複製代碼

cancelablePromiseChain 接受一個或多個返回Promise的函數,而後按順序調用,而且會被先前調用獲得返回值做爲參數,傳遞給下一個PromiseMaker函數,這個函數模擬了Promise鏈式調用,而後增長了中斷調用的能力。fetch

注意,finally函數,不只僅是一個語法糖,你不能夠在cancelablePromiseChain的最後一個參數寫一個PromiseMaker,而後期待它的行爲會和finally同樣,finally最重要的在於,它是 同步的,這保證了一旦回調鏈被取消或完成,finall回調被同步的馬上調用進行清理工做,若是是異步就會形成沒法預料的錯誤。很簡單的例子,若是是異步的,老的清理函數,可能後於新清理函數完成。spa

  • 示例
let lastFetchCancelToken = null;
fetchButton.on('click', () => {
    if (lastFetchCancelToken != null) = lastFetchCancelToken.cancel();
    lastFetchCancelToken = cancelablePromiseChain(
        () => fetchPost(),
        postData => updateView(postData),
    );
    // 試着想一想,若是finall是異步的,你能確定finally回調設置的lastFetchCancelToken,是本身那輪請求對應的lastFetchCancelToken嗎?
    lastFetchCancelToken.finally(() => lastFetchCancelToken = null);
});
複製代碼
  • 引入可取消後的版本
const lastRoundTranstionCancelTokenRef = useRef<ICancelToken | null>(null);
if (isIn) {
        if (lastRoundTranstionCancelTokenRef.current != null) lastRoundTranstionCancelTokenRef.current.cancel();
        lastRoundTranstionCancelTokenRef.current = cancelablePromiseChain(
          () => setClassName(`${initClassNameRef.current} enter`),
          () => setClassName(`${initClassNameRef.current} enter enter-active`),
          () => wait(timeout),
          () => setClassName(`${initClassNameRef.current} enter-done`),
        );
        // clear
        lastRoundTranstionCancelTokenRef.current.finally(() => lastRoundTranstionCancelTokenRef.current = null);

      } else {
        if (lastRoundTranstionCancelTokenRef.current != null) lastRoundTranstionCancelTokenRef.current.cancel();
        lastRoundTranstionCancelTokenRef.current = cancelablePromiseChain(
          () => setClassName(`${initClassNameRef.current} exit`),
          () => setClassName(`${initClassNameRef.current} exit exit-active`),
          () => wait(timeout),
          () => setClassName(`${initClassNameRef.current} exit-done`),
        );
        // clear
        lastRoundTranstionCancelTokenRef.current.finally(() => lastRoundTranstionCancelTokenRef.current = null);
      }
複製代碼

這裏說句題外話,相似本文探討的這種需求,最好的且簡單的解決方案是rxjs,然而我實在不想由於這一個簡單的需求,引入整個rxjs庫,就放棄了。code

相關文章
相關標籤/搜索