在几秒钟内逐步更新状态

时间:2019-08-05 18:52:41

标签: javascript reactjs state

我正在尝试模拟在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循环中的设置状态不是一个好主意,因为它没有时间快速渲染/更新。但是我认为这样做不是一个好主意,我不应该为此使用状态吗?

2 个答案:

答案 0 :(得分:1)

似乎您想添加请求的URL并将其记录在网页上。

因此,您似乎对使用for循环“强制性”更新状态感到困惑。
从本质上说,React更像是一个声明式的,您的组件通常会对“状态”更改做出反应。

因此,无需使用setTimeout循环(需要使用for循环),而是让我们“反应”设置的间隔状态改变。

类组件(CC)版本

正如我看到的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>
    );
  }
}

现在,您可以看到以给定间隔发送的渲染请求列表。

working demo

您可以继续使用CodeSandbox
Edit so.answer.57364530


功能组件(FC)版本

我还使用钩子(useStateuseEffect)在FC中实现了相同的CC版本。

随着useEffect在一个地方有效地“共置”(setIntervalclearInterval)问题,代码看起来更小了。

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> )} 的工作演示。

start stop buttons

答案 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();

希望这会有所帮助!