likes
comments
collection
share

竞态条件 (race condition)

作者站长头像
站长
· 阅读数 42

竞态条件(race condition),最早知道这个东西,是在公众号的一篇文章上。当时没有怎么分析这个东西,最近在看React 新版本官网的文档的时候,在useEffect这个hook文章时,又看到这个概念,在这里做下简单的分析及理解。

定义

竞态条件,从wiki上我们看到他是这样定义的。

是指一个系统或者进程的输出结果,依赖于多个不受控制的事件(也可以称为两个信号),两个事件相互竞争,来影响最终输出的结果。

在实际的开发过程中我们遇到这种情况,一般是在useEffect中进行fetch data的时候,直接上代码:

import { useEffect, useState } from "react";

export default function ArticleDetail(props: { id?: string }) {
  const { id } = props;
  const [title, setTitle] = useState<string>();
  const [fetchId, setFetchId] = useState<string>();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const getDetail = async () => {
      setLoading(true);
      const res = await fetch(`https://swapi.dev/api/people/${id}`);
      const data = await res.json();
      setFetchId(id);
      setTitle(data.name);
      setLoading(false);
    };
    getDetail();
  }, [id]);

  return (
    <div>
      <div className="title">存在竞态条件</div>
      <h3 style={{ color: fetchId === id ? "green" : "red" }}>
        FetchId: {fetchId}
      </h3>
      <div className="read-the-docs">
        用户名:{loading ? "加载中.." : title}
      </div>
    </div>
  );
}
function App() {
  const [currentId, setCurrentId] = useState<string>();

  const handleFetchData = () => {
    const newId = "" + Math.round(Math.random() * 80);
    setCurrentId(newId);
  };

  return (
    <div className="App">
      <div className="box">
        <button onClick={handleFetchData}>获取用户信息</button>
        <div>当前Fetch用户ID: {currentId}</div>
      </div>
      <hr />
      {currentId && <ArticleDetail id={currentId} />}
    </div>
  );
}

在App上述的代码中,我们通过控制currenId,让子组件ArticleDetail,进行fetch新数据渲染最新的用户信息。

当传入的ID和正在fetch的Id是一样的时候,我们会在让 FethcId :xxx变成绿色,否则为红色。

竞态条件的出现

当我们频繁的点击 btn 按钮时会出现下面的情况:

竞态条件 (race condition)

从上图可以看出,连续点击按钮时,会不断的更新请求的用户ID。但是ArticelDetail,对应的请求并没有按我们变换的顺序进行串连的请求,请求中谁是谁先请求到谁优先渲染,最后渲染的结果的FetchId和App组件中存储的ID是不一致的,最终的颜色为红色。

这里就出现了race conditions这种情况,多个fetch请求几乎同时触发,多个请求相互竞争谁先返回结果就先渲染谁,网络请求的情况是复杂的,且响应时间也是不确定的。最终影响了ArticleDetail的结果。

解决方案

在上面的代码中我们可以理解为,代码发送了几乎串行发送了请求。我们只想到最后一个请求的返回结果进行渲染。其它的请求可以取消或者忽略就达到我们的目标了。

忽略请求

我们可以使用 useEffect中支持return一个回调函数进行巧妙的处理。

export default function ArticleDetailRace(props: { id?: string }) {
  const { id } = props;
  const [title, setTitle] = useState<string>();
  const [fetchId, setFetchId] = useState<string>();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let fetchFlag = true;
    const getDetail = async () => {
      setLoading(true);
      const res = await window.fetch(`https://swapi.dev/api/people/${id}`);
      const data = await res.json();
      if(fetchFlag) {
        setFetchId(id);
        setTitle(data.name);
        setLoading(false);
      }
    };
    getDetail();
    return () => {
        fetchFlag = false;
    }
  }, [id]);

  return (
    <div>
        <div className="title">解决竞态条件后: </div>
       <h3 style={{ color: fetchId === id ? "green" : "red" }}>FetchId: {fetchId}</h3>
      <div className="read-the-docs">
        用户名:{loading ? "加载中.." : title}
      </div>
    </div>
  );

在useEffec中我们每次都会创建一个变量fetchFlag,在结果正常响应之后,判断这个值为true,则为我们想要渲染的结果,否则直接放弃。

根据hook执行规则,useEffect都先执行上次返回的函数,然后再执行本次render的逻辑。执行返回函数会把上次的fetchFlag为false,程序则不会处理api请求的结果。

取消请求

如果只是忽略请求反返回结果,并不是最好的处理方式。如果我在每次re-render的时候能取消上次的请求,便是最好的方案了。这就换成了如果取消promise请求的问题了。目前的方案有: 1. AbortController 2. CancelToken

这里以AbortController为主来说明:

export default function ArticleDetailAbort(props: { id?: string }) {
  const { id } = props;
  const [title, setTitle] = useState<string>();
  const [fetchId, setFetchId] = useState<string>();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const abortController = new AbortController();
    const getDetail = async () => {
      setLoading(true);
      const res = await window.fetch(`https://swapi.dev/api/people/${id}`, {
        signal: abortController.signal,
      });
      const data = await res.json();

      setFetchId(id);
      setTitle(data.name);
      setLoading(false);
    };
    getDetail();
    return () => {
      abortController.abort();
    };
  }, [id]);

  return (
    <div>
      <div className="title">解决竞态条件(AbortController)后: </div>
      <h3 style={{ color: fetchId === id ? "green" : "red" }}>
        FetchId: {fetchId}
      </h3>
      <div className="read-the-docs">
        用户名:{loading ? "加载中.." : title}
      </div>
    </div>
  );

当我们再多次点击按钮时对就的网络请求列表如下:

竞态条件 (race condition)

完美,即解决了竞态的问题。又避免了不必要的服务资源浪费。再看Can I Use目前浏览器上对这个属性兼容性:

竞态条件 (race condition)

应该没有太大问题,只要不是兼容IE浏览器的项目。

最后附上代码链接:stackblitz.com/edit/stackb…

总结

通过本文,我们了解了 race conditions 的概念,以及在React中出现的部分场景,并通过使用AbortController解决竞态问题。又学到一个新的知识点!

参考

  1. developer.mozilla.org/zh-CN/docs/…
  2. maxrozen.com/race-condit…
  3. mp.weixin.qq.com/s/J-er3Tei6…
  4. react.dev/reference/r…
转载自:https://juejin.cn/post/7245936314850476090
评论
请登录