多次请求相互覆盖的问题

952 阅读1分钟

刚在知乎上看到一道面试题。

某个应用模块由一个显示区域,以及按钮 A,按钮 B 组成。点击按钮 A,会向地址 uriA 发出一个
ajax 请求,并将返回的字符串填充到显示区域中,点击按钮 B,会向地址 uriB 发出一个ajax 请
求,并将返回的字符串再次填充到显示区域中(覆盖原有的数据)。
当用户依次点击按钮 AB 的时候,预期的效果是显示区域依次被 uriA、uriB 返回的数据填充,
但是由于到 uriA 的请求返回比较慢,导致 uriB 返回的数据被 uriA 返回的数据覆盖了,与用户预
期的顺序不一致。
请问如何设计代码,解决这个问题?至少说出两种方式

这个问题是我们在开发中经常会碰到的,以下提供三种思路,用Vue代码做演示。

  • 方法1:给每个请求做标记。
  • 方法2:将多个请求按顺序串联。
  • 方法3:提供取消方法,发送下一个请求时取消上个请求的回调。
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>

    <div class="space-x">
      <button @click="clickA">A</button>
      <button @click="clickB">B</button>
    </div>

    <div class="space-x">
      <button @click="clickA1">A1</button>
      <button @click="clickB1">B1</button>
    </div>

    <div class="space-x">
      <button @click="clickA2">A2</button>
      <button @click="clickB2">B2</button>
    </div>
  </div>
</template>

<script>

// 请求
const reqA = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("A"), 2000);
  });
};

const reqB = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("B"), 1000);
  });
};

// 方法3:取消封装
const cancelable = (req, callback) => {
  let cb = callback;
  req().then((value) => {
    cb && cb(value);
  });
  const cancel = () => {
    cb = undefined;
  };
  return cancel;
};

export default {
  name: "HelloWorld",
  data() {
    return {
      msg: "i",
      // 方法1:最后一个请求的标记
      current: 0,
      // 方法2:串联后的Promise
      currentPromise: Promise.resolve("i"),
      // 方法3:上个请求的取消方法
      lastCancel: undefined,
    };
  },
  methods: {
    // 方法1
    clickA() {
      const idx = ++this.current;
      reqA().then((value) => {
        if (idx === this.current) {
          this.msg = value;
        }
      });
    },
    clickB() {
      const idx = ++this.current;
      reqB().then((value) => {
        if (idx === this.current) {
          this.msg = value;
        }
      });
    },
    // 方法2
    clickA1() {
      this.currentPromise = this.currentPromise
        .then(() => reqA())
        .then((value) => {
          this.msg = value;
        });
    },
    clickB1() {
      this.currentPromise = this.currentPromise
        .then(() => reqB())
        .then((value) => {
          this.msg = value;
        });
    },
    // 方法3
    clickA2() {
      this.lastCancel && this.lastCancel();
      this.lastCancel = cancelable(reqA, (v) => (this.msg = v));
    },
    clickB2() {
      this.lastCancel && this.lastCancel();
      this.lastCancel = cancelable(reqB, (v) => (this.msg = v));
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}

.space-x > *:not(:first-child) {
  margin-left: 10px;
}

.space-x + .space-x {
  margin-top: 10px;
}
</style>

CodeSandbox链接