z-memo / interview

我们缺的从来都不是前端/后端工程师,而是工程师(或者那些会系统思考,并总是想着解决问题的人)
27 stars 3 forks source link

多个 tab 只对应一个内容框,点击每个 tab 都会请求接口并渲染到内容框,怎么确保频繁点击 tab 但能够确保数据正常显示? #210

Open MrSeaWave opened 2 years ago

MrSeaWave commented 2 years ago

一、分析

因为每个请求处理时长不一致,可能会导致先发送的请求后响应,即请求响应顺序和请求发送顺序不一致,从而导致数据显示不正确。

即可以理解为连续触发多个请求,如何保证请求响应顺序和请求发送顺序一致。对于问题所在场景,用户只关心最后数据是否显示正确,即可以简化为:连续触发多个请求,如何保证最后响应的结果是最后发送的请求(不关注之前的请求是否发送或者响应成功)

类似场景:input 输入框即时搜索,表格快速切换页码

二、解决方案

防抖(过滤掉一些非必要的请求) + 取消上次未完成的请求(保证最后一次请求的响应顺序)

取消请求方法:

伪代码(以 setTimeout 模拟请求,clearTimeout 取消请求)

/**
 * 函数防抖,一定时间内连续触发事件只执行一次
 * @param {*} func 需要防抖的函数
 * @param {*} delay 防抖延迟
 * @param {*} immediate 是否立即执行,为true表示连续触发时立即执行,即执行第一次,为false表示连续触发后delay ms后执行一次
 */
let debounce = function (func, delay = 100, immediate = false) {
  let timeoutId, last, context, args, result;

  function later() {
    const interval = Date.now() - last;
    if (interval < delay && interval >= 0) {
      timeoutId = setTimeout(later, delay - interval);
    } else {
      timeoutId = null;
      if (!immediate) {
        result = func.apply(context, args);
        context = args = null;
      }
    }
  }

  return function () {
    context = this;
    args = arguments;
    last = Date.now();

    if (immediate && !timeoutId) {
      result = func.apply(context, args);
      context = args = null; // 解除引用
    }

    if (!timeoutId) {
      timeoutId = setTimeout(later, delay);
    }

    return result;
  };
};

let flag = false; // 标志位,表示当前是否正在请求数据
let xhr = null;

let request = (i) => {
  if (flag) {
    clearTimeout(xhr);
    console.log(`取消第${i - 1}次请求`);
  }
  flag = true;
  console.log(`开始第${i}次请求`);
  xhr = setTimeout(() => {
    console.log(`请求${i}响应成功`);
    flag = false;
  }, Math.random() * 200);
};

let fetchData = debounce(request, 50); // 防抖

// 模拟连续触发的请求
let count = 1;
let getData = () => {
  setTimeout(() => {
    fetchData(count);
    count++;
    if (count < 11) {
      getData();
    }
  }, Math.random() * 200);
};
getData();

/* 某次测试输出:
    开始第2次请求
    请求2响应成功
    开始第3次请求
    取消第3次请求
    开始第4次请求
    请求4响应成功
    开始第5次请求
    请求5响应成功
    开始第8次请求
    取消第8次请求
    开始第9次请求
    请求9响应成功
    开始第10次请求
    请求10响应成功
*/
MrSeaWave commented 2 years ago

类似 antd 中的某个带有远程搜索,防抖控制,请求时序控制,加载状态的多选示例

import { Select, Spin } from 'antd';
import debounce from 'lodash/debounce';

const { Option } = Select;

class UserRemoteSelect extends React.Component {
  constructor(props) {
    super(props);
    this.lastFetchId = 0;
    this.fetchUser = debounce(this.fetchUser, 800);
  }

  state = {
    data: [],
    value: [],
    fetching: false,
  };

  fetchUser = value => {
    console.log('fetching user', value);
    this.lastFetchId += 1;
    const fetchId = this.lastFetchId;
    this.setState({ data: [], fetching: true });
    fetch('https://randomuser.me/api/?results=5')
      .then(response => response.json())
      .then(body => {
        if (fetchId !== this.lastFetchId) {
          // for fetch callback order
          return;
        }
        const data = body.results.map(user => ({
          text: `${user.name.first} ${user.name.last}`,
          value: user.login.username,
        }));
        this.setState({ data, fetching: false });
      });
  };

  handleChange = value => {
    this.setState({
      value,
      data: [],
      fetching: false,
    });
  };

  render() {
    const { fetching, data, value } = this.state;
    return (
      <Select
        mode="multiple"
        labelInValue
        value={value}
        placeholder="Select users"
        notFoundContent={fetching ? <Spin size="small" /> : null}
        filterOption={false}
        onSearch={this.fetchUser}
        onChange={this.handleChange}
        style={{ width: '100%' }}
      >
        {data.map(d => (
          <Option key={d.value}>{d.text}</Option>
        ))}
      </Select>
    );
  }
}

ReactDOM.render(<UserRemoteSelect />, mountNode);