React 之 Race Condition
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
竞态条件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 中常见的例子。
这是一个非常典型的数据获取代码:
classArticleextendsComponent{
state={
article:null
componentDidMount(){
this.fetchData(this.props.id);
}
asyncfetchData(id){
constarticle=awaitAPI.fetchArticle(id);
this.setState({article
}
//...
}
看起来没什么问题,但这段代码还没有实现数据更新,我们再改一下:
classArticleextendsComponent{
state={
article:null
componentDidMount(){
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps){
if(prevProps.id!==this.props.id){
this.fetchData(this.props.id);
}
}
asyncfetchData(id){
constarticle=awaitAPI.fetchArticle(id);
this.setState({article
}
//...
}
当组件传入新的id时,我们根据新的id请求数据,然后setState最新获取的数据。
这时就可能出现竞态条件,比如用户选完立刻点击下一页,我们请求id为 1 的数据,紧接着请求id为 2 的数据,但因为网络或者接口处理等原因,id为 2 的接口提前返回,便会先展示id为 2 的数据,再展示id为 1 的数据,这就导致了错误。
我们可以想想遇到这种问题的场景,比如类似于百度的搜索功能,切换 tab 等场景,虽然我们也可以使用诸如 debounce 的方式来缓解,但效果还是会差点,比如使用 debounce,用户在输入搜索词的时候,展示内容会长期处于空白状态,对于用户体验而言,我们可以做的更好。
那么我们该如何解决呢?一种是在切换的时候取消请求,还有一种是借助一个布尔值来判断是否需要更新,比如这样:
functionArticle({id}){
const[article,setArticle]=useState(null);
useEffect(()={
letdidCancel=false;
asyncfunctionfetchData(){
constarticle=awaitAPI.fetchArticle(id);
//如果didCancel为true说明用户已经取消了
if(!didCancel){
setArticle(article);
}
}
fetchData();
//执行下一个effect之前会执行
return()={
didCancel=true;
},[id]);
//...
}
当然你也可以用 ahooks 中的useRequest,它的内部有一个 ref 变量记录最新的 promise,也可以解决 Race Condition 的问题:
functionArticle({id}){
const{data,loading,error}=useRequest(()=fetchArticle(id),{
refreshDeps:[id]
//...
}
效果演示问题复现为了方便大家自己测试这个问题,我们提供相对完整的代码。以《Avoiding Race Conditions when Fetching Data with React Hooks》中的例子为例,出现 Race Condition 问题的代码如下:
constfakeFetch=person={
returnnewPromise(res={
setTimeout(()=res(`${person}'sdata`),Math.random()*5000);
};
constApp=()={
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]);
consthandleClick=(name)=()={
setPerson(name)
}
return(
Fragment
buttononClick={handleClick('Nick')}Nick'sProfile/button
buttononClick={handleClick('Deb')}Deb'sProfile/button
buttononClick={handleClick('Joe')}Joe'sProfile/button
{person(
Fragment
h1{person}/h1
p{loading?'Loading...':data}/p
/Fragment
)}
/Fragment
};
我们实现了一个fakeFetch函数,用于模拟接口的返回,具体返回的时间为Math.random() * 5000),用于模拟数据的随机返回。
实现效果如下:从效果图中可以看到,我们按顺序点击了Nick、Deb、Joe,理想情况下,结果应该显示Joe's Data,但最终显示的数据为最后返回的Nick's Data。
布尔值解决现在,我们尝试用一个canceled布尔值解决:
constApp=()={
const[data,setData]=useState('');
const[loading,setLoading]=useState(false);
const[person,setPerson]=useState(null);
useEffect(()={
letcanceled=false;
setLoading(true);
fakeFetch(person).then(data={
if(!canceled){
setData(data);
setLoading(false);
}
return()=(canceled=true);
},[person]);
return(
Fragment
buttononClick={()=setPerson('Nick')}Nick'sProfile/button
buttononClick={()=setPerson('Deb')}Deb'sProfile/button
buttononClick={()=setPerson('Joe')}Joe'sProfile/button
{person(
Fragment
h1{person}/h1
p{loading?'Loading...':data}/p
/Fragment
)}
/Fragment
};
实现效果如下:即便接口没有按照顺序返回,依然不影响最终显示的数据。
useRequest 解决我们也可以借助ahooks的useRequest方法,修改后的代码如下:
constApp2=()={
const[person,setPerson]=useState('Nick');
const{data,loading}=useRequest(()=fakeFetch(person),{
refreshDeps:[person],
consthandleClick=(name)=()={
setPerson(name)
}
return(
Fragment
buttononClick={handleClick('Nick')}Nick'sProfile/button
buttononClick={handleClick('Deb')}Deb'sProfile/button
buttononClick={()=setPerson('Joe')}Joe'sProfile/button
{person(
Fragment
h1{person}/h1
p{loading?'Loading...':data}/p
/Fragment
)}
/Fragment
};
代码效果如上,就不重复录制了。
考虑到部分同学可能会对useRequest的使用感到困惑,我们简单介绍一下useRequest的使用:
useRequest的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的loading、data、error等状态。
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
functionwrapPromise(promise){
letstatus="pending";
letresult;
letsuspender=promise.then(
r={
status="success";
result=
},
e={
status="error";
result=
}
return{
read(){
if(status==="pending"){
throwsuspender;
}elseif(status==="error"){
throwresult;
}elseif(status==="success"){
returnresult;
}
}
}
constfakeFetch=person={
returnnewPromise(res={
setTimeout(()=res(`${person}'sdata`),Math.random()*5000);
};
functionfetchData(userId){
returnwrapPromise(fakeFetch(userId))
}
constinitialResource=fetchData('Nick');
functionUser({resource}){
constdata=resource.read();
returnp{data}/p
}
constApp=()={
const[person,setPerson]=useState('Nick');
const[resource,setResource]=useState(initialResource);
consthandleClick=(name)=()={
setPerson(name)
setResource(fetchData(name));
}
return(
Fragment
buttononClick={handleClick('Nick')}Nick'sProfile/button
buttononClick={handleClick('Deb')}Deb'sProfile/button
buttononClick={handleClick('Joe')}Joe'sProfile/button
Fragment
h1{person}/h1
Suspensefallback={'loading'}
Userresource={resource}/
/Suspense
/Fragment
/Fragment
};
而关于 Suspense 的具体讲解,详见下篇。
React 系列React 之 createElement 源码解读React 之元素与组件的区别React 之 Refs 的使用和 forwardRef 的源码解读React 之 Context 的变迁与背后实现React 系列的预热系列,带大家从源码的角度深入理解 React 的各个 API 和执行过程,全目录不知道多少篇,预计写个 50 篇吧。
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线