竞态条件 (race condition)
竞态条件(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 按钮时会出现下面的情况:
从上图可以看出,连续点击按钮时,会不断的更新请求的用户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>
);
当我们再多次点击按钮时对就的网络请求列表如下:
完美,即解决了竞态的问题。又避免了不必要的服务资源浪费。再看Can I Use目前浏览器上对这个属性兼容性:
应该没有太大问题,只要不是兼容IE浏览器的项目。
最后附上代码链接:stackblitz.com/edit/stackb…
总结
通过本文,我们了解了 race conditions
的概念,以及在React
中出现的部分场景,并通过使用AbortController
解决竞态问题。又学到一个新的知识点!
参考
转载自:https://juejin.cn/post/7245936314850476090