写一个解决竞态问题的高阶函数

1,685 阅读3分钟

什么是竞态?如何优雅的解决接口响应乱序的问题?如何取消fetch()触发的网络请求?一个高阶函数就能搞定

什么是竞态

首先,什么是异步任务的竞态?前端遇到竞态的场景与后端略有不同,处理方式也各不相同,但是发生竞态的根源是一致的,那就是异步任务的不可控和不可预测。请看图

假设用户每次点击查询按钮都会触发一次网络请求(依次为task1、task2、task3、task4、task5)。然而由于网络状况的不可控,以及每次查询的条件可能不同(比如task3查到了100条数据,而task5只查到2条),最后触发的请求有可能先于其他请求得到了响应。

这时就出现了状态的竞争,因为很有可能你在task5响应时渲染的结果,很快就被task4的响应覆盖了,又很快被task3的响应覆盖了。

按照常规的业务设计,我们应该只保留task5的查询结果。这就需要在新的task开始时,结束先前已经发起的竞争task

理想的结果如图:

想要实现以上效果,可以利用XMLHttpRequest.abort() 、或者借助Axios等三方库来实现。使用fetch发起的网络请求是无法在客户端终止的。但是我们可以换个思路,封装一个可被打断的Promise。

可被打断的Promise

在业务开发中,一个可被打断的Promise有时能起到事半功倍的作用。

function task() {
  let cancel;
  const cancelablePromise = new Promise(function (resolve, reject) {
    cancel = reject
    // do sth.
    do_sth()
  })
  cancelablePromise.cancel = cancel;
  return cancelablePromise;
}

const p = task();
p.cancel();

需要注意的是,do_sth() 函数的执行并没有被打断。被打断的只是const p ,它是一个promise。如果在调用p.cancel()之前,p的状态已经变成了resolved,那么p.cancel不起任何作用。

用高阶函数隔离脏操作

如上所述,在外部打断promise是一种脏操作,使用时必须极为谨慎。

接下来,介绍如何通过高阶函数的封装,把脏操作隔离到一个相对安全的空间。来一个野鸡变凤凰。

function autoCancel(fn, onCancelErr = null) {
  const cancelSymbol = Symbol('cancel');
  let running;
  return async function r(...args) {
    running && running()
    running = null;
    const result = await Promise.race([fn(...args), (function () {
      return new Promise(function (resolve) {
        running = resolve.bind(undefined, cancelSymbol)
      })
    })()]);
    if (result === cancelSymbol) {
      if (onCancelErr) {
        throw onCancelErr
      }
      return null;
    }
    return result;
  }
}

如上,20行代码,就实现了一个高阶函数,内部封印了一头能量巨大的野兽。

原理很简单,利用了Promise.race的特性,通过抢先resolve第二个promise来实现打断。当上一个任务还没有完成,而新的任务又被创建时,直接返回一个undefined(或者抛错onCancelErr)。

下面,就来看一下如何驾驭这头野兽:

const queryList = autoCancel(async function(param) {
  const resp = await fetch(`/query?name=${param}`);
  return await resp.json();
}, new Error('canceled'));

try {
  const result = await queryList('helo');
} catch(e) {
  // do sth
}

同样需要注意的是,被中断的只是一个promise,真正的网络请求并没有被终止,它还是会正常返回,只是返回结果被忽略了。

例子

下面是一个完整的例子:

如上图,task5最后开始却最先完成。应该忽略task1-4的执行结果,只保留task5的返回值。

完整示例代码

最后

如果我的分享对你有帮助,可以关注我的公众号DevStudio, 第一时间查收优质技术分享。