setTimeout在执行回调代码时比没有使用它时慢得多。

7
我们在使用Javascript中的setTimeout函数时,遇到了一个奇怪的问题,尤其是在React中使用代码时,使用setTimeout比不使用setTimeout运行得慢得多!与不使用setTimeout相比,性能的差异如下:

使用setTimeout1391 毫秒
不使用setTimeout15 毫秒

在API回调中(例如axios),也会出现使用setTimeout的情况!

下面显示了一个简单的样例代码:
codesandbox

有谁能解释一下发生了什么?
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [value, setValue] = useState("");
  const [time, setTime] = useState("");

  const startSample = () => {
    const startTimeStamp = new Date().valueOf();
    for (let i = 0; i < 5000; i++) {
      setValue(`test-${i}`);
      // console.log(`test-${i}`);
    }
    const endTimeStamp = new Date().valueOf();
    setTime(`${endTimeStamp - startTimeStamp}ms`);
  };

  const handleClick1 = () => {
    startSample();
  };

  const handleClick2 = () => {
    setTimeout(() => {
      startSample();
    });
  };

  return (
    <div style={{ textAlign: "left" }}>
      <p>{value || "Please push that button!"}</p>
      <div>
        <button id="startBtn" onClick={handleClick1}>
          Start Normal
        </button>
        <button id="startBtn1" onClick={handleClick2}>
          Start With setTimeout
        </button>
      </div>
      <p>Result: {time}</p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

6
这是一个老问题。当使用setTimeout包装时,实际上会生成5000次渲染而不仅仅是一次。 - Dupocas
两者的执行方式相同,不同之处在于React不会批处理未在React事件处理程序内触发的更新。请参见我回答中M Erikson的引用。 - Dupocas
回调函数像axios这样的解决方案是什么,它就像setTimeout版本一样。 - Siyavash Hamdi
同样在我的回答中。使用累加器并仅调用一次setState。 - Dupocas
2个回答

7

React会批处理排队在事件回调函数内的渲染,因此在单击处理程序中对setValue的所有调用都会导致一个单一的渲染,一旦处理程序完成。

但是,我认为React不会对setTimeout调用的渲染进行批处理。因此,在setTimeout处理程序中对setValue的每个调用后都会进行渲染。

请参见此问题:https://github.com/facebook/react/issues/14259

您可以通过以下方式更快地编写setTimeout版本:

const handleClick2 = () => {
  setTimeout(() => ReactDOM.unstable_batchedUpdates(() => startSample()));
}

如果您有一堆异步的 ajax 响应返回,并希望在它们全部到达后应用它们,那么您可能会编写以下代码:
const [a, setA] = useState();
const [b, setB] = useState();
const [c, setC] = useState();
const [d, setD] = useState();

useEffect(() => {
  (async () => {
    const a = await fetchA();
    const b = await fetchB();
    const c = await fetchC();
    const d = await fetchD();

    // wrap all of the state updates in batchUpdates
    // so that we only get one render instead of 4
    ReactDOM.unstable_batchUpdates(() => {
      setA(a);
      setB(b);
      setC(c);
      setD(d);
    });
  })()
}), []);

两个代码在相同情况下执行。唯一区别是使用或不使用 setTimeout - Siyavash Hamdi
1
是的,setTimeout 调用导致您的更新在事件处理程序之外运行,因此 React 不会批量渲染。unstable_batchUpdates 是让您的代码“重新回到”React批量渲染上下文的方法。 - Brandon
Brandon,像axios这样的回调函数有什么类似于setTimeout版本的解决方案吗? - Siyavash Hamdi
2
在调用ReactDOM.unstable_batchedUpdates时运行您的状态更新批处理。我会发布一个简短的示例,但是如果没有看到您的代码,我不知道它是否适合。 - Brandon

4

React目前会在React事件内,例如按钮点击或输入更改中,批量处理状态更新。但如果它们是在React事件处理程序之外触发的,例如setTimeout(),则不会批量处理更新。

考虑以下示例:

  const startSample = () => {
    const startTimeStamp = new Date().valueOf();
    const arr = []
    for (let i = 0; i < 5000; i++) {
      arr.push(i)
      // console.log(`test-${i}`);
    }
    setValue(arr)
    const endTimeStamp = new Date().valueOf();
    setTime(`${endTimeStamp - startTimeStamp}ms`);
  };

现在两个时间是一样的,对吧?正如Brandon指出的那样,React似乎不会等待所有更新完成后再触发新的渲染,导致产生了5000次渲染而不是只有一次。因此,使用一个累加器来执行迭代并仅设置状态一次。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接