React Hook 搞定 Race Condition

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

Race Condition 是开发中经常遇到的问题,比如查询天气的时候,先输入“北京”,再输入“深圳”,这时将发起 2 个请求。很可第一个请求花的时间比第二个请求长,如果不做处理,最终看到的是北京的天气,而不是深圳。本文要讨论的就是如何使用 React Hooks 解决这种问题。

二、场景

假设有如下搜索的场景,当用户输入关键字的时候,系统根据关键字搜索,然后实时显示搜索结果。代码如下:

// 模拟网络请求,可以指定延迟时间
function getData(data, delay) {
  return new Promise(resolve => {
    setTimeout(()=>{
      resolve(`${data} result`);
    }, delay);
  })
}

function App() {
  const [query, setQuery] = useState('react');
  const [result, setResult] = useState();

  useEffect(() => {
    const fetchData = async () => {
      // 搜索 react1 时(第一个请求),2 秒后返回,其余 500 毫秒后返回
      const delay = query === 'react1' ? 2000 : 500;
      
      const result = await getData(query, delay);
      
      setResult(result); 
    }

    fetchData();
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <div>
        result: <span>{result}</span>
      </div>
    </Fragment>
  );
}

当我们输入react12345时,可以看到最终的结果是react1 result,而我们期望看到的结果是react12345 result

这现象的原因是更新数据的时候,没有对结果的有效性进行判断,用过期的数据覆盖了最新的数据。

三、解决方案

解决方式很简单,就是在更新数据前判断其有效性,改造一下useEffect部分的代码:

useEffect(() => {
  // 有效性标识
  let didCancel = false;

  const fetchData = async () => {
    const delay = query === 'react1' ? 2000 : 500;
    const result = await getData(query, delay);

    // 更新数据前判断有效性
    if (!didCancel) {
      setResult(result); 
    }
  }

  fetchData();

  return () => {
    // query 变更时设置数据失效
    didCancel = true;
  }
}, [query]);

这里利用了useEffect数据清理的特性,当 query 发生变化时,将之前的数据请求设置为失效。

四、更好的方案

上面这种方案虽然解决了问题,但体验并不好。在输入数据的过程中,并不能看到输入过程中返回的结果,只能看到最终的结果。我们期望的效果是输入过程中能实时展示有效的结果。再改造下代码:

// 请求序号
let seqenceId = 0;
// 上一个有效请求的序号
let lastId = 0;

function App() {
  const [query, setQuery] = useState('react');
  const [result, setResult] = useState();

  useEffect(() => {
    const fetchData = async () => {
      // 发起一个请求时,序号加 1
      const curId = ++seqenceId;

      const delay = query === 'react1' ? 2000 : 500;
      const result = await getData(query,delay);

      // 只展示序号比上一个有效序号大的数据
      if (curId > lastId) {
        setResult(result); 
        lastId = curId;
      } else {
        console.log(`discard ${result}`); 
      }
    }

    fetchData();
  }, [query]);

  return (
    ...
  );
}

这里引入了 2 个变量,一个变量用来标识当前请求的序号,另一个记录上一个有效请求的序号。只有序号比上一个有效序号大的时候,才展示数据。这样就保证了旧的请求不会覆盖新的请求。

最终的代码可以看这里

五、总结

本文讨论了开发过程中经常遇到的 Race Condition 问题,结合 React Hooks 给出了 2 种解题思路。一种是只展示最终的结果,隐藏过程结果;另一种是将过程中有效的结果也展示出来。可以根据实际的应用场景按需选用。


睿Talk
记录个人和团队技术成长的点点滴滴
5.5k 声望
418 粉丝
0 条评论
推荐阅读
Web前端主题切换的几种方案
这种方案利用了css多层样式精确匹配的特点,通过样式覆盖的方式实现主题的切换。首先需要在应用的根元素中设一个 class,切换主题时给 class 赋上对应的值,下面以theme1/theme2为例。

Dickens8阅读 7.5k

封面图
深入理解React Diff算法
fiber上的updateQueue经过React的一番计算之后,这个fiber已经有了新的状态,也就是state,对于类组件来说,state是在render函数里被使用的,既然已经得到了新的state,那么当务之急是执行一次render,得到持有新...

nero31阅读 11.7k评论 3

【WebRTC 跨端通信】React + React Native 双端视频聊天、屏幕共享
之前介绍过 WebRTC,简单来说它是一个点对点的实时通讯技术,主要基于浏览器来实现音视频通信。这项技术目前已经被广泛应用于实时视频通话,多人会议等场景。

杨成功14阅读 2k评论 1

封面图
把React新文档投喂给 GPT-4 后...
大家好,我卡颂。最近,React新文档终于上线了。从内容上看,新文档包括:理论知识、学习指引API介绍从形式上看,新文档除了传统的文字内容,还包括:在线Demo示意图小测验可以说是阅读体验拉满。但是,由于文档...

卡颂7阅读 1k评论 2

封面图
PDF 预览和下载你是怎么实现的?
在开发过程中要求对 PDF 类型的发票提供 预览 和 下载 功能,PDF 类型文件的来源又包括 H5 移动端 和 PC 端,而针对这两个不同端的处理会有些许不同,下文会有所提及。

熊的猫7阅读 1.2k评论 1

封面图
React为什么不将Vite作为默认推荐?
CRA推出于2016年,彼时还没有成体系的React脚手架工具供大家使用,再加上这是官方工具,一经推出就受到了欢迎。截止当前,CRA仓库已经收获快10wstar。

卡颂4阅读 1.2k

封面图
第九期:前端九条启发分享
下图是一个常见的列表, 点击列表里的详情按钮会跳到详情页, 那么也许我们在详情页修改了数据状态, 此时可能需要把修改后的状态直接传给列表页从而本地直接更新列表, 这样就不用发送新的api请求与后端交互了。

lulu_up6阅读 223

5.5k 声望
418 粉丝
宣传栏