我正在尝试模拟在Web应用程序上运行的脚本。这个想法是脚本在后台运行并正在启动http请求,然后这些请求应在div中显示为<p>
。但我不希望它进行得太快。首先,过快地更新状态不会正确地更新状态,其次,它使用户难以看到。
我已经尝试过使用setTimeout,但是这不起作用,因为它似乎正在设置超时,然后不等待就继续设置setState。
AddParagraph = () => {
for(var i = 0; i < 20; i++){
setTimeout(function(){
this.setState({
urls: [...this.state.urls, www.url.com/i]
})},2000)
}
}
我已经读到for循环中的设置状态不是一个好主意,因为它没有时间快速渲染/更新。但是我认为这样做不是一个好主意,我不应该为此使用状态吗?
答案 0 :(得分:1)
似乎您想添加请求的URL并将其记录在网页上。
因此,您似乎对使用for
循环“强制性”更新状态感到困惑。
从本质上说,React更像是一个声明式的,您的组件通常会对“状态”更改做出反应。
因此,无需使用setTimeout
循环(需要使用for
循环),而是让我们“反应”设置的间隔状态改变。
正如我看到的this.state
,我假设您正在使用抄送。
class ClassRequestViewer extends Component {
state = {
urls: [],
postId: 1
};
// To be used to clear the interval
// ? to prevent memory leaks
intervalId = undefined;
// Add a paragraph while setting the new post ID
// ? together in one shot.
addParagraph = () => {
const newUrl = `${urlRoot}/${this.state.postId}`;
this.setState(prevState => ({
urls: prevState.urls.concat(newUrl),
postId: prevState.postId + 1
}));
};
// ? You would want to initialize the interval only ONCE
// as the changing the state in `addParagraph` will
// cause `ClassRequestViewer` to re-render (in `render()` below).
componentDidMount() {
this.intervalId = setInterval(this.addParagraph, timeout);
}
// ⚠? is important!
// We need to make sure to clean up to prevent memory leak.
// OR else, the interval will run even if the component is unmounted
// and unavailable.
componentWillUnmount() {
this.intervalId && clearInterval(this.intervalId);
}
// ? Whenever "addParagraph" updates the state,
// This gets called, so will show the requests sent automatically.
render() {
const { urls } = this.state;
return (
<ul style={{ listStyle: "none" }}>
{urls.map(url => (
<li key={url}>Request sent to {url}</li>
))}
</ul>
);
}
}
现在,您可以看到以给定间隔发送的渲染请求列表。
我还使用钩子(useState
,useEffect
)在FC中实现了相同的CC版本。
随着useEffect
在一个地方有效地“共置”(setInterval
和clearInterval
)问题,代码看起来更小了。
function RequestViewer() {
const [urls, setUrls] = useState([]);
const [postId, setPostId] = useState(1);
useEffect(() => {
function addParagraph() {
const newUrl = `${urlRoot}/${postId}`;
setUrls(_ => _.concat(newUrl));
setPostId(postId + 1);
}
const intervalId = setInterval(addParagraph, timeout);
return () => clearInterval(intervalId);
}, [postId]);
return (
<ul style={{ listStyle: "none" }}>
{urls.map(url => (
<li key={url}>Request sent to {url}</li>
))}
</ul>
);
}
那里也没有for
循环,这是因为useEffect
每当postId
的{{1}}秒被更改时,timeout
都会被更新。
我希望它在onClick事件上启动,并在X行数后停止。这似乎无休止地运行吗?
正如您在comment中所提到的,它连续运行。
为了能够“开始/结束”该过程,您需要将“查看器”保留为查看器并处理父级上的显示。
因此,当您“启动”该过程时,查看器setInterval
将启动请求并向您显示结果,然后单击“停止”将卸载该组件(这就是我使用ClassRequestViewer
的原因到componentWillUnmount
)。
我已经将clearInterval
更新为具有App
按钮。
start/stop
当您单击function App() {
const [isStarted, setIsStarted] = useState(false);
return (
<div className="App">
<h1>Request Viewer</h1>
{!isStarted && (
<button onClick={() => setIsStarted(true)}>Start the request</button>
)}
{isStarted && (
<>
<button onClick={() => setIsStarted(false)}>Stop Requests</button>
<ClassRequestViewer />
</>
)}
</div>
);
}
按钮时,它将Start the request
设置为isStarted
,从而加载了true
,您可以看到请求。
以下代码已加载
ClassRequestViewer
当您单击{isStarted && (
<>
<button onClick={() => setIsStarted(false)}>Stop Requests</button>
<ClassRequestViewer />
</>
)}
按钮时,该按钮会将Stop Request
设置为isStarted
,因此false
会卸载,(依次调用ClassRequestViewer
进行清理上)。
然后您再次看到componentWillUnmount -> clearInterval
按钮。
Start the request
下面是更新后的{!isStarted && (
<button onClick={() => setIsStarted(true)}>Start the request</button>
)}
的工作演示。
答案 1 :(得分:0)
这里的问题是,在启动所有异步操作(此处为for
)时,setTimeout
循环会立即运行到完成。
这是因为for
循环在继续循环的下一个迭代之前没有等待异步操作完成,并且因为setTimout回调在将来的某个时候被调用。因此,循环完成其迭代,然后触发setTimeout的所有回调同时更新状态,从而立即引起竞争状况。另外,请记住,setState
本身是异步的。
您可以使用promise和async / await来使代码同步并按照您期望的方式运行-
const delay = (time, i) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Setting state", i);
this.setState({
urls: [...this.state.urls, `www.url.com/${i}`]
});
resolve();
}, time);
});
};
const addParagraph = async () => {
for (let i = 0; i < 10; i++) {
await delay(1000, i);
}
};
addParagraph();
希望这会有所帮助!