简述useCallback、 useReducer、 useContext

1,809

useCallback

在了解useCallback之前先来看一个案例

// parentComponent.js

import React, { useState } from "react";
import { ChildComponent } from "./childComponent";

export function ParentComponent() {
  console.log("ParentComponent running");
  const [parentCount, setParent] = useState(1);
  const parentCountHandler = () => {
    setParent(parentCount + 1);
  };
  const childHandler = () => {
    console.log("childHandler", parentCount);
  };
  return (
    <div>
      <div onClick={parentCountHandler}>change parentCount</div>
      <ChildComponent onClickChild={childHandler} />
    </div>
  );
}
// childComponent.js
import React from "react";
export const ChildComponent = (props) => {
  console.log("ChildComponent running");
  return <div onClick={props.onClickChild}>ChildComponent</div>;
};

当我们点击[change parentCount]改变parentCount的值时,控制台打印的结果如下:

image.png

可以看出当parentCount改变的时候ParentComponent和ChildComponent全部re render了。但其实子组件ChildComponent并没有使用parentCount,所以子组件的重新渲染并不是我们期望的。那么想要阻止ChildComponent re render我们肯定会想到PureComponent、memo、shouldComponentUpdate,这三种方法的目的都是通过对父级传递的props进行浅比较来控制子组件的re render。 接下来我们用其中的memo来处理以下子组件

import React, { memo } from "react";
// 使用memo包裹住函数组件
export const ChildComponent = memo((props) => {
  console.log("ChildComponent running");
  return <div onClick={props.onClickChild}>ChildComponent</div>;
});

image.png 遗憾的是发现输出结果依然是子组件进行了re render。为什么会这样? 因为父组件传递给子组件是一个函数childHandler,而函数每次都会被重新创建,分配新的内存地址,这样子组件通过对props.onClickChild的浅比较结果自然是props.onClickChild发生了改变,因此依然会执行re render。 接下来该useCallback上场了

import React, { useCallback, useState } from "react";
import { ChildComponent } from "./childComponent";

export function ParentComponent() {
  console.log("ParentComponent running");
  const [parentCount, setParent] = useState(1);
  const parentCountHandler = () => {
    setParent(parentCount + 1);
  };
  // 使用useCallback包裹住回调函数
  const childHandler = useCallback(() => {
    console.log("childHandler", parentCount);
  }, []);
  return (
    <div>
      <div onClick={parentCountHandler}>change parentCount</div>
      <ChildComponent onClickChild={childHandler} />
    </div>
  );
}

image.png useCallback上场后我们终于得到了期望的结果,父组件自己re render7次,子组件岿然不动。

再来分析以下useCallback为什么能做到。

因为useCallback真正的作用是缓存了每次渲染是的回调函数的实例,只有useCallback的参数中的值发生了变化才会重新创建,否则就一直不变。而子组件通过浅比较发现props的值没有变化,自然就不会重新渲染。这也就是为什么useCallback和useMemo需要成对使用的原因。

useReducer

当然除了使用useCallback可以避免子组件不必要的渲染,我们也可以考虑useReducer

import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";

export function ParentComponent() {
  console.log("ParentComponent running");
  const [parentCount, setParent] = useState(1);
  const parentCountHandler = () => {
    setParent(parentCount + 1);
  };
  // useReducer替代了原来的useCallback
  const [_, childHandler] = useReducer(() => {
    console.log("childHandler", parentCount);
  });
  return (
    <div>
      <div onClick={parentCountHandler}>change parentCount</div>
      <ChildComponent onClickChild={childHandler} />
    </div>
  );
}

以上代码可以实现和useCallback一样的效果。因为React会保证dispatch始终是不变的。

useContext

我们再来看一个比较复杂的例子方便我们引出useContext

import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";


const students = [];
const changeStudents = (oldData, action) => {
  switch (action.type) {
    case "add":
      return [...oldData, { name: action.data.name }];
    case "delete":
      return oldData.filter((item) => item.name !== action.data.name);
    default:
      return oldData;
  }
};`


export function ParentComponent() {
  console.log("ParentComponent running");
  const [parentCount, setParent] = useState(1);
  const parentCountHandler = () => {
    setParent(parentCount + 1);
  };
  const [studentsState, onSubmit] = useReducer(changeStudents, students);


  return (
    <div>
      <div onClick={parentCountHandler}>change parentCount</div>
      <ChildComponent onSubmit={onSubmit} />
      {studentsState.map((child) => (
        <div key={child.name}>{child.name}</div>
      ))}
    </div>
  );
}
import React, { memo } from "react";
import { AddComponent } from "./addComponent";
import { DeleteComponent } from "./deleteComponent";
export const ChildComponent = memo((props) => {
  console.log("ChildComponent running");
  return (
    <div>
      <AddComponent onSubmit={props.onSubmit} />
      <DeleteComponent onSubmit={props.onSubmit} />
    </div>
  );
});
import React, { useRef, useState } from "react";
export const AddComponent = (props) => {
  const [text, setText] = useState("");
  const inputRef = useRef();
  return (
    <>
      <input
        ref={inputRef}
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <div
        onClick={() => {
          props.onSubmit({ type: "add", data: { name: text } });
          inputRef.current.value = "";
        }}
      >
        add
      </div>
    </>
  );
};
import React from "react";
export const DeleteComponent = (props) => {
  return (
    <div
      onClick={() => {
        props.onSubmit({ type: "delete", data: { name: "李雷" } });
      }}
    >
      delete
    </div>
  );
};

image.png

通过上边的代码,我们实现的是一个三级组件的嵌套,通过最顶级ParentComponent传递给最子组件的子组件AddComponent和DeleteComponent回调方法,来修改父组件中的studentsState值。其中我为了把回调函数传递给第三级,我先把函数传递给了第二级。实际开发中那如果级别很多的话,可能就要继续层层传递下去,那这样估计你往上找的时候一定会头大。别着急,我们还没让useContext出场呢,再来修改以下代码

import React, { useReducer, useState } from "react";
import { ChildComponent } from "./childComponent";


const students = [];
const changeStudents = (oldData, action) => {
  switch (action.type) {
    case "add":
      return [...oldData, { name: action.data.name }];
    case "delete":
      return oldData.filter((item) => item.name !== action.data.name);
    default:
      return oldData;
  }
};
export const ParentContext = React.createContext(); // 创建了ParentContext


export function ParentComponent() {
  console.log("ParentComponent running");
  const [parentCount, setParent] = useState(1);
  const parentCountHandler = () => {
    setParent(parentCount + 1);
  };
  const [studentsState, onSubmit] = useReducer(changeStudents, students);


  return (
    <ParentContext.Provider value={onSubmit}> //使用了 ParentContext.Provider传递onSubmit
      <div>
        <div onClick={parentCountHandler}>change parentCount</div>
        <ChildComponent /> // 删除了给ChildComponet传递的回调函数
        {studentsState.map((child) => (
          <div key={child.name}>{child.name}</div>
        ))}
      </div>
    </ParentContext.Provider>
  );
}
import React, { memo } from "react";
import { AddComponent } from "./addComponent";
import { DeleteComponent } from "./deleteComponent";
export const ChildComponent = memo((props) => {
  console.log("ChildComponent running");
  return (
    <div>
      <AddComponent /> // 删除了回调函数的传递
      <DeleteComponent /> // 删除了回调函数的传递
    </div>
  );
});
import React, { useContext, useRef, useState } from "react";
import { ParentContext } from "./parentComponent";
export const AddComponent = (props) => {
  const [text, setText] = useState("");
  const inputRef = useRef();
  const onSubmit = useContext(ParentContext); // 通过useContext获取onSubmit
  return (
    <>
      <input
        ref={inputRef}
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <div
        onClick={() => {
          onSubmit({ type: "add", data: { name: text } });
          inputRef.current.value = "";
        }}
      >
        add
      </div>
    </>
  );
};
import React, { useContext } from "react";
import { ParentContext } from "./parentComponent";
export const DeleteComponent = (props) => {
  const onSubmit = useContext(ParentContext);  // 通过useContext获取onSubmit
  return (
    <div
      onClick={() => {
        onSubmit({ type: "delete", data: { name: "李雷" } });
      }}
    >
      delete
    </div>
  );
};

验证一下你会发现执行结果是一样的,但是却解决了层层传递回调函数的烦恼。