浅谈可取消的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

相关文章
相关标签/搜索