Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React useState、setState 同步异步问题 #44

Open
imfenghuang opened this issue Jun 20, 2024 · 0 comments
Open

React useState、setState 同步异步问题 #44

imfenghuang opened this issue Jun 20, 2024 · 0 comments

Comments

@imfenghuang
Copy link
Owner

React useState、setState 同步异步问题

react 18

在 react 18 版本中,在 createRoot 模式下,useState、setState 无论是在同步方法中调用,还是在异步方法中调用,setState 都是异步的,原因是 react 18 的 createRoot 下有 auto batching(自动批处理)机制。即在同一次渲染时,合并更新 state。

reactwg/react-18#21

// APP 组件
function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // react 18 自动批处理
      setCount(c => c + 1);
      setFlag(f => !f);
      // 只会 re-render 一次
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

// 表现相同
function handleClick() {
  // 方式 1
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // 只会 re-render 一次

  // 同方式 2
  setTimeout(() => {
    setCount((c) => c + 1);
    setFlag((f) => !f);
    // 只会 re-render 一次
  }, 1000);

  // 同方式 3
  fetch(/*...*/).then(() => {
    setCount((c) => c + 1);
    setFlag((f) => !f);
    // 只会 re-render 一次
  });
}

// 方式 4
elm.addEventListener("click", () => {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // 只会 re-render 一次
});

// 主入口
import ReactDOM from 'react-dom/client';
import App from './App.jsx';

// createRoot
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
react 18 如何同步更新

可以使用 flushSync 进行同步更新。注意,这种方式如非必要,并不推荐使用。因为过多的同步更新会带来性能的损耗。

import { flushSync } from "react-dom";

const handleClick6 = () => {
    Promise.resolve().then(() => {
      console.log("handleClick6 0 :", count);
      flushSync(() => {
        console.log(1);
        setCount((count) => count + 1);
        console.log("-> 1", count);
      });
      console.log("handleClick6 1 :", count);
      flushSync(() => {
        console.log(2);
        setCount((count) => count + 1);
        console.log("-> 2", count);
      });
      console.log("handleClick6 2 :", count);
    });
  };

<div onClick={handleClick6}>flushSync {count} </div>

0

在 react 16.8/17 版本中,setState 同步异步需要区分看

react 16.8/17

先说结论:

  • 在 react 16.8/17 的异步代码中,无论是合成事件,还是原生事件,setState 是同步更新的,即一次 updateState 会触发一次 re-render
  • 在 react 16.8/17 的同步代码中,在 合成事件的同步代码 中,setState 是异步更新的
// App.jsx
import { useState,  useEffect } from "react";

function App() {
  const [count, setCount] = useState(0);

  // 合成同步
  const handleClick = () => {
    console.log("---- 执行合成同步事件1 start ----")
    console.log("handleClick 0 :", count);
    setCount(count + 1);
    console.log("handleClick 1 :", count);
    setCount(count + 1);
    console.log("handleClick 2 :", count);
    console.log("---- 执行合成同步事件1 end ----")
  };

  // 合成同步
  const handleClick2 = () => {
    console.log("---- 执行合成同步事件 2 start ----")
    console.log("handleClick2 0 :", count);
    setCount((count) => count + 1);
    console.log("handleClick2 1 :", count);
    setCount((count) => count + 1);
    console.log("handleClick2 2 :", count);
    console.log("---- 执行合成同步事件 2 end ----")
  };

  // 合成异步
  const handleClick3 = () => {
    console.log("---- 执行合成同步事件 3 start ----")
    setTimeout(() => {
      console.log("---- 执行合成同步事件 3 定时器内 ----")
      console.log("handleClick3 0 :", count);
      setCount(count + 1);
      console.log("handleClick3 1 :", count);
      setCount(count + 1);
      console.log("handleClick3 2 :", count);
      console.log("---- 执行合成同步事件 3 end ----")
    }, 1000);
  };

  console.log("render");

  useEffect(() => {
    document.getElementById("div").addEventListener("click", () => {
      console.log("---- 执行原生同步事件 1 start ----")
      console.log("原生1 0 :", count);
      setCount(count + 1);
      console.log("原生1 1 :", count);
      setCount(count + 1);
      console.log("原生1 2 :", count);
      console.log("---- 执行原生同步事件 1 end ----")
    });

    document.getElementById("div2").addEventListener("click", () => {
      console.log("---- 执行原生同步事件 2 start ----")
      console.log("原生2 0 :", count);
      setCount((count) => count + 1);
      console.log("原生2 1 :", count);
      setCount((count) => count + 1);
      console.log("原生2 2 :", count);
      console.log("---- 执行原生同步事件 2 end ----")
    });

    document.getElementById("div3").addEventListener("click", () => {
      console.log("---- 执行原生异步事件 3 start ----")
      setTimeout(() => {
        console.log("---- 执行原生异步事件 3 定时器内 ----")
        console.log("原生3 0 :", count);
        setCount((count) => count + 1);
        console.log("原生3 1 :", count);
        setCount((count) => count + 1);
        console.log("原生3 2 :", count);
        console.log("---- 执行原生异步事件 3 end ----")
      });
    });
  }, []);

  return (
    <>
      <div>--- 合成事件 ---</div>
      <div onClick={handleClick}>合成同步事件 1 {count} </div>
      <div onClick={handleClick2}>合成同步事件 2 {count} </div>
      <div onClick={handleClick3}>合成异步事件 3 {count} </div>
      <hr/>
      <div>--- 原生事件 ---</div>
      <div id="div">原生同步事件 1 {count}</div>
      <div id="div2">原生同步事件 2 {count}</div>
      <div id="div3">原生异步事件 3 {count}</div>
    </>
  );
}

export default App;

// 主入口
import ReactDOM from 'react-dom';
import App from './App.jsx';

ReactDOM.render(<App />, document.getElementById('root'));

// package.json
"dependencies": {
  "react": "^17.0.2",
  "react-dom": "^17.0.2"
}

1.合成事件 - 同步

1

2

2.合成事件 - 异步

3

3.原生事件 - 同步

4

5

4.原生事件 - 异步

6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant