React竞态条件Race Condition实例详解

目录
  • 竞态条件
  • React 与竞态条件
  • 效果演示
    • 问题复现
    • 布尔值解决
    • useRequest 解决
  • Suspense

竞态条件

Race Condition,中文译为竞态条件,旨在描述一个系统或者进程的输出,依赖于不受控制事件的出现顺序或者出现时机。

举个简单的例子:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"
   // 如果其他的线程在 "if (x == 5)" and "y = x * 2" 执行之间更改了 x 的值
   // y 就可能不等于 10.
}

你可能想,JavaScript 是单线程,怎么可能出现这个问题?

React 与竞态条件

确实如此,但前端有异步渲染,所以竞态条件依然有可能出现,我们举个 React 中常见的例子。

这是一个非常典型的数据获取代码:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

看起来没什么问题,但这段代码还没有实现数据更新,我们再改一下:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

当组件传入新的 id 时,我们根据新的 id 请求数据,然后 setState 最新获取的数据。

这时就可能出现竞态条件,比如用户选完立刻点击下一页,我们请求 id 为 1 的数据,紧接着请求 id 为 2 的数据,但因为网络或者接口处理等原因,id为 2 的接口提前返回,便会先展示 id 为 2 的数据,再展示 id 为 1 的数据,这就导致了错误。

我们可以想想遇到这种问题的场景,比如类似于百度的搜索功能,切换 tab 等场景,虽然我们也可以使用诸如 debounce 的方式来缓解,但效果还是会差点,比如使用 debounce,用户在输入搜索词的时候,展示内容会长期处于空白状态,对于用户体验而言,我们可以做的更好。

那么我们该如何解决呢?一种是在切换的时候取消请求,还有一种是借助一个布尔值来判断是否需要更新,比如这样:

function Article({ id }) {
  const [article, setArticle] = useState(null);
  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
      // 如果 didCancel 为 true 说明用户已经取消了
      if (!didCancel) {
        setArticle(article);
      }
    }
    fetchData();
    // 执行下一个 effect 之前会执行
    return () => {
      didCancel = true;
    };
  }, [id]);
  // ...
}

当然你也可以用 ahooks 中的 useRequest,它的内部有一个 ref 变量记录最新的 promise,也可以解决 Race Condition 的问题:

function Article({ id }) {
  const { data, loading, error} = useRequest(() => fetchArticle(id), {
  	refreshDeps: [id]
  });
  // ...
}

效果演示

问题复现

为了方便大家自己测试这个问题,我们提供相对完整的代码。以 《Avoiding Race Conditions when Fetching Data with React Hooks》中的例子为例,出现 Race Condition 问题的代码如下:

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);
  useEffect(() => {
    setLoading(true);
    fakeFetch(person).then(data => {
      	setData(data);
      	setLoading(false);
    });
  }, [person]);
    const handleClick = (name) => () => {
    	setPerson(name)
    }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
      <button onClick={handleClick('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};

我们实现了一个 fakeFetch函数,用于模拟接口的返回,具体返回的时间为 Math.random() * 5000),用于模拟数据的随机返回。

实现效果如下:

从效果图中可以看到,我们按顺序点击了 NickDebJoe,理想情况下,结果应该显示 Joe's Data,但最终显示的数据为最后返回的 Nick's Data

布尔值解决

现在,我们尝试用一个 canceled 布尔值解决:

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);
  useEffect(() => {
    let canceled = false;
    setLoading(true);
    fakeFetch(person).then(data => {
      if (!canceled) {
        setData(data);
        setLoading(false);
      }
    });
    return () => (canceled = true);
  }, [person]);
  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
      <Fragment>
        <h1>{person}</h1>
        <p>{loading ? 'Loading...' : data}</p>
      </Fragment>
    )}
    </Fragment>
  );
};

实现效果如下:

即便接口没有按照顺序返回,依然不影响最终显示的数据。

useRequest 解决

我们也可以借助 ahooksuseRequest 方法,修改后的代码如下:

const App2 = () => {
  const [person, setPerson] = useState('Nick');
  const { data, loading} = useRequest(() => fakeFetch(person), {
    refreshDeps: [person],
  });
  const handleClick = (name) => () => {
    setPerson(name)
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
      <Fragment>
        <h1>{person}</h1>
        <p>{loading ? 'Loading...' : data}</p>
      </Fragment>
    )}
    </Fragment>
  );
};

代码效果如上,就不重复录制了。

考虑到部分同学可能会对 useRequest 的使用感到困惑,我们简单介绍一下 useRequest的使用:

useRequest 的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loadingdataerror 等状态。

useRequest 同样提供了一个 options.refreshDeps 参数,当它的值变化后,会重新触发请求。

const [userId, setUserId] = useState('1');
const { data, run } = useRequest(() => getUserSchool(userId), {
  refreshDeps: [userId],
});

上面的示例代码,useRequest 会在初始化和 userId 变化时,触发函数执行。与下面代码实现功能完全一致:

const [userId, setUserId] = useState('1');
const { data, refresh } = useRequest(() => getUserSchool(userId));
useEffect(() => {
  refresh();
}, [userId]);

Suspense

这篇之所以讲 Race Condition,主要还是为了引入讲解 Suspense,借助 Suspense,我们同样可以解决 Race Condition:

// 实现参考的 React 官方示例:https://codesandbox.io/s/infallible-feather-xjtbu
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}
const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
function fetchData(userId) {
  return wrapPromise(fakeFetch(userId))
}
const initialResource = fetchData('Nick');
function User({ resource }) {
  const data = resource.read();
  return <p>{ data }</p>
}
const App = () => {
  const [person, setPerson] = useState('Nick');
  const [resource, setResource] = useState(initialResource);
  const handleClick = (name) => () => {
    setPerson(name)
    setResource(fetchData(name));
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
	    <button onClick={handleClick('Joe')}>Joe's Profile</button>
      <Fragment>
        <h1>{person}</h1>
        <Suspense fallback={'loading'}>
          <User resource={resource} />
        </Suspense>
      </Fragment>
    </Fragment>
  );
};

以上就是React竞态条件Race Condition实例详解的详细内容,更多关于React竞态条件Race Condition的资料请关注我们其它相关文章!

(0)

相关推荐

  • React Context 变迁及背后实现原理详解

    目录 Context 老的 Context API 基础示例 context 中断问题 解决方案 新的 Context API 基础示例 模拟实现 createContext 源码 Context 本篇我们讲 Context,Context 可以实现跨组件传递数据,大部分的时候并无需要,但有的时候,比如用户设置 了 UI 主题.地区偏好,如果从顶层一层层往下传反而有些麻烦,不如直接借助 Context 实现数据传递. 老的 Context API 基础示例 在讲最新的 API 前,我们先回顾下老

  • React元素与组件的区别示例详解

    目录 从问题出发 元素与组件 元素 组件 问题如何解决 自定义内容 第一种实现方式 第二种实现方式 第三种实现方式 从问题出发 我被问过这样一个问题: 想要实现一个 useTitle 方法,具体使用示例如下: function Header() { const [Title, changeTitle] = useTitle(); return ( <div onClick={() => changeTitle('new title')}> <Title /> </div

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • react app rewrited替代品craco使用示例

    目录 1. 不使用custom-cra的原因 2. craco基本使用 3. 使用craco修改antd主题 4. 别名 5. babel扩展 6. 分包 7. 配置代理 8. 最后 1. 不使用custom-cra的原因 custom-cra,react-app-rewired 与 craco 都是用来无 eject 重写 CRA 配置 custom-cra上次更新在两年前,有些配置跟不上新的版本,例如使用webpack5配置less会出错, 虽说目前有了解决方案引入新包customize-c

  • concent渐进式重构react应用使用详解

    目录 正文 需求来了 准备工作 UI 实现 消灭生命周期函数 提升状态到store 解耦业务逻辑与UI 爱class,爱hook,让两者和谐共处 使用组件 结语 正文 传统的redux项目里,我们写在reducer里的状态一定是要打通到store的,我们一开始就要规划好state.reducer等定义,有没有什么方法,既能够快速享受ui与逻辑分离的福利,又不需要照本宣科的从条条框框开始呢?本文从普通的react写法开始,当你一个收到一个需求后,脑海里有了组件大致的接口定义,然后丝滑般的接入到co

  • es6中使用map简化复杂条件判断操作实例详解

    本文实例讲述了es6中使用map简化复杂条件判断操作.分享给大家供大家参考,具体如下: 复杂逻辑判断时需要写很多if/else,代码可读性较差,可以用es6新增的Map来简化代码 列举六种实例,逐步简化 /** * 按钮点击事件 * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 */ const onButtonClick = (status) => { if (status == 1) { sendLog('pr

  • shell脚本语言之if条件判断语句实例详解

    目录 1.单分支if条件语句 1.1举例:判断目录是否存在,不存在则创建 2.双分支if条件语句 2.1举例:监听并自动重启apache服务脚本 3.多分支if条件语句 3.1举例:判断用户输入的是文件还是目录 4.case条件语句 4.1举例:创建启动脚本,让service命令管理apache 4.2举例:创建启动脚本,让service命令管理nginx 总结 1.单分支if条件语句 then后面跟符合条件之后执行的程序,可以放在[]之后,用;分隔.也可以换行写入, 就不需要“;”了. 比如:

  • React Suspense解决竞态条件详解

    目录 前言 Suspense 执行机制 实际应用 好处:请求前置 好处:解决竞态条件 错误处理 源码 前言 在上一篇<React 之 Race Condition>中,我们最后引入了 Suspense 来解决竞态条件问题,本篇我们来详细讲解一下 Suspense. Suspense React 16.6 新增了 <Suspense> 组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面(像是个 spinner),让它在用户等待的时候显示. 目前,Suspense 仅支

  • react中的ajax封装实例详解

    react中的ajax封装实例详解 代码块 **opts: {'可选参数'} **method: 请求方式:GET/POST,默认值:'GET'; **url: 发送请求的地址, 默认值: 当前页地址; **data: string,json; **async: 是否异步:true/false,默认值:true; **cache: 是否缓存:true/false,默认值:true; **contentType: HTTP头信息,默认值:'application/x-www-form-urlenc

  • js中自定义react数据验证组件实例详解

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

  • 对python字典过滤条件的实例详解

    如下所示: d = { 'a': '0.0000', 'b': '1.2' } d_tmp = dict((key, value) for key, value in d.items() if float(value) > 0) print(d_tmp) output: {'b': '1.2'} 以上这篇对python字典过滤条件的实例详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • Java如何在临界区中避免竞态条件

    当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件.导致竞态条件发生的代码区称作临界区.在临界区中使用适当的同步就可以避免竞态条件. 界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现.synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C. D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C .D)运行完

  • React中编写CSS实例详解

    目录 正文 内联样式 普通的CSS css modules css in js 样式组件 引入外部变量 默认值 引入全局样式 provider 样式继承 动态添加class 正文 目前,前端最流行的开发方式是组件化,而CSS的设计本身就不是为组件化而生的,所以在目前组件化的框架中都在需要一种合适的CSS解决方案 在组件化开发环境下的CSS,应该满足如下需求: 可以编写局部css: css具备自己的具备作用域,不会随意污染其他组件内的元素 可以编写动态的css: 可以获取当前组件的一些状态,根据状

  • React 悬浮框内容懒加载实例详解

    目录 界面隐藏 懒加载 React实现 原始代码 放入新的DIV 状态设置 样式设置 事件设置 事件优化 延迟显示悬浮框 悬浮框内容懒加载 完整代码 界面隐藏 一个容器放置视频,默认情况下 display: none; z-index: 0; transform: transform3d(10000px, true_y, true_z); y轴和z轴左边都是真实的(腾讯视频使用绝对定位,因此是计算得到的),只是将其移到右边很远的距离. 懒加载 React监听鼠标移入(获取坐标) 添加事件监听 o

  • Java8 新特性Lambda表达式实例详解

    Java8 新特性Lambda表达式实例详解 在介绍Lambda表达式之前,我们先来看只有单个方法的Interface(通常我们称之为回调接口): public interface OnClickListener { void onClick(View v); } 我们是这样使用它的: button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { v.setText("

随机推荐