什么是竞态条件?

当你的程序依赖正确的响应顺序,但响应的顺序又无法保证时,可能会导致意外的结果,这就是竞态条件。

举个用户界面的例子:
image.png
在上图中,分别点击科技和生活tab时,期望能够展示对应的数据。

我们来设想如下情况:

  1. 点击科技按钮(高亮科技按钮)(数据2秒后返回)
  2. 点击生活按钮(高亮生活按钮)(数据1秒后返回)

最终,展示区域会在1秒后显示生活数据,2秒后展示科技数据,但此时,我们的按钮会高亮会在生活上,但我们的展示区域显示的却是科技数据。也就是高亮的按钮和展示数据不同步,这就是竞态条件导致的问题。

如何避免?

方案1:每次操作完成之前,阻止新的操作。
这个方案用的比较普遍,具体思路就是请求发生期间,添加一个loading遮罩层,这样在当前请求响应之前,后续的操作都会被loading遮罩层避免掉,也就不会有竞态问题的发生。

方案2:每次发送请求时,丢掉上一个请求的响应。
该方案的思路是,在响应完成之前,如果用户有新的请求,那就丢弃掉未完成的请求,其结果就是只对最新的请求进行响应,也就避免出现旧的请求响应数据展示在了当前高亮视图下。

function getResolveWhenLast() {
    let globalId = 0;

    return (pro) => {
      return new Promise((resolve, reject) => {
        const id = ++globalId;

        pro
          .then((res) => {
            if (id === globalId) {
                resolve(res);
            }
          })
          .catch(err => {
            if (id === globalId) {
                reject(err);
            }
          })
      })
    }
}

const resolveWhenLast = getResolveWhenLast()

// 使用resolveWhenLast包住你的请求,就可以解决竞态问题
resolveWhenLast(api.getPosts()).then(res => {
    // ...
})

方案3:每次发送请求时,取消掉上一次的请求。
该方案的思路是,给promise加了一层包装,添加cancel的能力,每次请求发出的时候,将上一次的请求取消掉,同样达到了只最处理最新一次请求响应的目的,也就避免了竞态条件的发生。

// 给Promise添加取消请求的能力
function createImpretivePromise(pro) {
  let resolve = null;
  let reject = null;

  const warppedPromise = new Promise((_resolve, _reject) => {
      resolve = _resolve;
      reject = _reject;
  })

   pro
    .then((res) => {
      resolve && resolve(res);
    })
    .catch((err) => {
      reject && reject(err);
    });

  // 可以切断响应
  const cancel = () => {
    resolve = null;
    reject = null;
  }

  return {
    promise: warppedPromise,
    cancel
  }
}

const getResolveWhenLast = () => {
  let globalCancel = null;

  return (pro) => {
    const { promise, cancel } = createImperativePromise(pro);

    globalCancel && globalCancel();
    globalCancel = cancel;

    return promise;
  };
};

const resolveWhenLast = getResolveWhenLast()

// 使用resolveWhenLast包住你的请求,就可以解决竞态问题
resolveWhenLast(api.getPosts()).then((res) => {
    // ...
});

热饭班长
3.7k 声望434 粉丝

先去做,做出一坨狗屎,再改进。