前言

本人使用了 hooks 已经有一段时间了,经常使用的 useState , useEffect,useRef基本已经掌握了,但是对优化和其他的 hooks 并没有很好的运用起来,下面简单介绍一下 hooks 的优化使用

优化篇

useCallback

useCallback 常用的场景是缓存一个 function,再 render 过程中不再重新创建。

下面我们看一些例子:

对 useEffect 进行优化:

  1. export default function App() {
  2. const [count, setCount] = useState(0);
  3. const clg = ()=>{
  4. console.log(2)
  5. }
  6. React.useEffect(()=>{
  7. clg()
  8. },[clg])
  9. return (
  10. <div className="App">
  11. <div>{count}</div>
  12. <button
  13. onClick={() => {
  14. setCount((prev)=> prev + 1)
  15. }}
  16. >
  17. Click
  18. </button>
  19. </div>
  20. );
  21. }

我们点击按钮后都会 render 重新创建 clg 函数,从而使 useEffect 再次执行,如果我们并不需要 clg 变化而影响 useEffect 执行,我们该怎么做呢?

  1. export default function App() {
  2. const [count, setCount] = useState(0);
  3. // callback会缓存这个函数
  4. const clg = React.useCallback(()=>{
  5. console.log(2)
  6. },[])
  7. React.useEffect(()=>{
  8. clg()
  9. },[clg])
  10. return (
  11. <div className="App">
  12. <div>{count}</div>
  13. <button
  14. onClick={() => {
  15. setCount((prev)=> prev + 1)
  16. }}
  17. >
  18. Click
  19. </button>
  20. </div>
  21. );
  22. }

我们使用 useCallback 来缓存这个函数,从而 useEffect 不再进行进行无用的执行。

搭配 memo 进行优化:

  1. function One({clg}) {
  2. clg()
  3. return <h2>One</h2>;
  4. }
  5. export default function App() {
  6. const [count, setCount] = useState(0);
  7. const clg = ()=>{
  8. console.log(2)
  9. }
  10. return (
  11. <div className="App">
  12. <div>{count}</div>
  13. <button
  14. onClick={() => {
  15. setCount((prev)=> prev + 1)
  16. }}
  17. >
  18. Click
  19. </button>
  20. <One clg={clg} />
  21. </div>
  22. );
  23. }

我们将 clg 以属性的形式传递给子组件,每次点击按钮后都会使子组件更新。

  1. const MemoOne = React.memo(function One({clg}}) {
  2. clg()
  3. return <h2>MemoOne</h2>;
  4. });
  5. export default function App() {
  6. const [count, setCount] = useState(0);
  7. const clg = React.useCallback(()=>{
  8. console.log(2)
  9. },[])
  10. return (
  11. <div className="App">
  12. <div>{count}</div>
  13. <button
  14. onClick={() => {
  15. setCount((prev)=> prev + 1)
  16. }}
  17. >
  18. Click
  19. </button>
  20. <MemoOne clg={clg} />
  21. </div>
  22. );
  23. }

memo 为我们提供浅层比较方法,与 class 组件的 shouldComponentUpdate 生命周期类似,

useMemo

如果 useCallback 是缓存 function,那 useMemo 就是缓存用来属性或函数结果等具体的值,与 vue 的 computed 类似。

  1. function One({sum}) {
  2. console.log(sum)
  3. return <h2>One</h2>;
  4. }
  5. export default function App() {
  6. const [count, setCount] = useState(0);
  7. const sum = 2 + 3
  8. return (
  9. <div className="App">
  10. <div>{count}</div>
  11. <button
  12. onClick={() => {
  13. setCount((prev)=> prev + 1)
  14. }}
  15. >
  16. Click
  17. </button>
  18. <One sum={sum} />
  19. </div>
  20. );
  21. }

每次 render 都会重新渲染子组件

  1. const MemoOne = React.memo(function One({sum}) {
  2. console.log(sum) // 5
  3. return <h2>MemoOne</h2>;
  4. });
  5. export default function App() {
  6. const [count, setCount] = useState(0);
  7. // 这里注意 useMemo 必须传一个有返回值的函数
  8. const sum = useMemo(()=>{
  9. return 2 + 3
  10. },[])
  11. return (
  12. <div className="App">
  13. <div>{count}</div>
  14. <button
  15. onClick={() => {
  16. setCount((prev)=> prev + 1)
  17. }}
  18. >
  19. Click
  20. </button>
  21. <MemoOne sum={sum} />
  22. </div>
  23. );
  24. }

useMemo 配合 memo 实现子组件无效的渲染

memo

useMemo 与 useCallback 优化后的属性,子组件都需要memo进行包裹,否则无效。因为无状态组件自身并不会浅比较对比属性是否变化。 需要注意的是与class 组件中 [shouldComponentUpdate()](https://zh-hans.reactjs.org/docs/react-component.html#shouldcomponentupdate) 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反

  1. function MyComponent(props) {
  2. /* 使用 props 渲染 */
  3. }
  4. function areEqual(prevProps, nextProps) {
  5. /*
  6. 如果把 nextProps 传入 render 方法的返回结果与
  7. 将 prevProps 传入 render 方法的返回结果一致则返回 true,
  8. 否则返回 false
  9. */
  10. }
  11. export default React.memo(MyComponent, areEqual);

合并更新

react 17.0 之后合成事件场景下在进行批量更新

  1. import { useState, useLayoutEffect } from "react";
  2. import * as ReactDOM from "react-dom";
  3. function App() {
  4. const [count, setCount] = useState(0);
  5. const [flag, setFlag] = useState(false);
  6. function handleClick() {
  7. console.log("=== click ===");
  8. setCount((c) => c + 1);
  9. setFlag((f) => !f);
  10. // 只批量更新一次
  11. }
  12. console.log("ReRender");
  13. return (
  14. <div>
  15. <button onClick={handleClick}>点我</button>
  16. <h1>{count}</h1>
  17. </div>
  18. );
  19. }
  20. const rootElement = document.getElementById("root");
  21. ReactDOM.render(<App />, rootElement);

在异步和 setTimeout 场景会造成 hooks 批量更新失败,导致多次 render

  1. import { useState, useLayoutEffect } from "react";
  2. import * as ReactDOM from "react-dom";
  3. function fetchSomething() {
  4. return new Promise((resolve) => setTimeout(resolve, 100));
  5. }
  6. function App() {
  7. const [count, setCount] = useState(0);
  8. const [flag, setFlag] = useState(false);
  9. function handleClickPromise() {
  10. console.log("=== click ===");
  11. fetchSomething().then(() => {
  12. setCount((c) => c + 1);
  13. setFlag((f) => !f);
  14. // 更新两次
  15. });
  16. }
  17. function handleClickSetTimeout() {
  18. setTimeout(() => {
  19. setCount((c) => c + 1); // Causes a re-render
  20. setFlag((f) => !f); // Causes a re-render
  21. // 更新两次
  22. }, 0)
  23. }
  24. console.log("Rerender: 正在重新渲染");
  25. return (
  26. <div>
  27. <button onClick={handleClickPromise}>Promise</button>
  28. <button onClick={handleClickSetTimeout}>SetTimeout</button>
  29. <h1>{count}</h1>
  30. </div>
  31. );
  32. }
  33. const rootElement = document.getElementById("root");
  34. ReactDOM.render(<App />, rootElement);

手动批量更新

react-dom 中提供了unstable_batchedUpdates方法进行手动批量更新

  1. const handerClick = () => {
  2. Promise.resolve().then(()=>{
  3. unstable_batchedUpdates(()=>{
  4. setCount((c) => c + 1);
  5. setFlag((f) => !f);
  6. // 仅更新一次
  7. })
  8. })
  9. }

参考:

https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo

https://juejin.cn/post/6908895801116721160#heading-37