likes
comments
collection
share

React 18 新特性之自动批量更新

作者站长头像
站长
· 阅读数 49

1 什么是批量更新

1.1 概念

批量更新(Batching):将多次 state 更新合并到一次 render。

1.2 优点

  • 提升性能;
  • 避免意外bug。当需要更新多个state时,如果每更新一个 state 就 render,或将会出现“半完成”的情况,可能造成异常。

1.3 举例

举例来看什么是批量更新:Demo地址

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

  function handleClick() {
    // re-render 一次
    setCount((c) => c + 1);     setFlag((f) => !f);   }

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

function LogEvents(props) {
  console.log("Render");
  return null;
}

// 结果:点击一次 button,只会打印一个"Render" 

2 批量更新的时机

2.1 差异

React 18 对批量更新的时机做了调整:

  • 在 React 18 之前,React 仅在浏览器事件处理期间才会做批量更新,而在其他事件(比如:Promise、setTimeout、native 事件)期间是不会执行的。
  • 在 React 18 版本中,并且使用 ReactDOM.createRoot,则 React 会在所有事件中进行自动批量更新(Automatic Batching) 。而如果使用ReactDOM.render,则会依旧维持之前版本的表现。

备注:为了介绍起来方便,后文 React 18 均代指 React 18+使用 createRoot 的情况。

2.2 举例

React 18 之前:Demo 地址

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

  function handleClick() {
    setTimeout(() => {       setCount((c) => c + 1);       setFlag((f) => !f);     }, 0);
  }

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

function LogEvents(props) {
  console.log("Render");
  return null;
}

// 结果:点击一次 button,会打印两个"Render" 

React 18:createRoot Demo 地址

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

  function handleClick() {
    setTimeout(() => {       setCount((c) => c + 1);       setFlag((f) => !f);     }, 0);
  }

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

function LogEvents(props) {
  console.log("Render");
  return null;
}

const rootElement = document.getElementById("root");
// 结果:React 18 新特性。点击一次 button,只会打印一个"Render" ReactDOM . createRoot(rootElement).render(<App />);

// 结果:维持之前的表现。点击一次 button,会打印两个"Render"
 // ReactDOM.render(<App />, rootElement);

2.3 不同事件中的 state 更新

看完上文,不知你是否会产生一个新的疑问 —— 对于不同事件里的 state 更新,React 18 会做自动批处理吗?

于是我们尝试了几种其他的情形:Demo 地址

  function handleClick() {
    setTimeout(() => {
      setCount((c) => c + 1);
    }, 0);
    setTimeout(() => {
      setFlag((f) => !f);
    }, 0);
  }
  // 结果:点击一次 button,只会打印一个"Render" 
  function handleClick() {
    setTimeout(() => {
      setCount((c) => c + 1);
    }, 0);
    setTimeout(() => {
      setFlag((f) => !f); 
    }, 100);
  }
  // 结果:点击一次 button,会打印两个"Render" 
  function handleClick() {
    setCount((c) => c + 1);
    
    setTimeout(() => {
      setFlag((f) => !f);
    }, 0);
  }
   // 结果:点击一次 button,会打印两个"Render" 

React 只在安全的情况下批量更新,会确保对于发起的事件,DOM 在下一个事件之前完全更新。

3 如果不想使用自动批量更新

如果既想使用 React 18 ,但又不想使用自动批量更新这个特性(非必要情况不太建议这么做)。可以选择这么做:

3.1 策略一:使用 render

3.1 节中,我们提到“React 18 中只有使用 ReactDOM.createRoot 才会触发自动批量更新”。相应的,如果使用ReactDOM.render,则会依旧维持之前版本的表现。

3.2 策略二:使用 flushSync

可以使用 ReactDOM.flushSync将 state 更新分开包裹

举例:Demo 地址

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  flushSync(() => {
    setFlag(f => !f);
  });
}

// 结果:点击一次 button,会打印两个"Render" 

4 特殊 case & 注意点

4.1 在类组件的异步事件中更新 state

之前,Class 组件在异步事件中更新 state,会有一个边界 case,如下:

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

在 React 18 中,因为异步事件中的 state 更新也做了批处理,因此解决了这个边界 case

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

二者对比的 Demo 地址

但是,如果你希望 React 18 中,类组件的这个 case 依旧保持,你可以使用上文提到的flushSync

建议:非必要不要这么做

Demo 地址

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

4.2 unstable_batchedUpdates

React 18 之前有个冷门 APIunstable_batchedUpdates ,可以用它来做批量更新,现在 React 18 已经支持自动批量更新了,因此没必要使用它了。React 暂时不会移除这个 API,但未来可能会移除。

5 总结

React 18 之前只会对浏览器事件内的 state 更新做批处理,但 React 18 起能够对所有事件(浏览器事件、异步事件等)的 state 更新做自动的批处理。

参考文档

Automatic batching for fewer renders in React 18

转载自:https://juejin.cn/post/7153814771937067044
评论
请登录