React系列:useCallback & useMemo

82 阅读5分钟

1. useCallback

作用

  • 简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数),在父子组件传参或者通用函数封装中,起到举足轻重的作用。
  • 用于需要传递给子组件的函数,减少子组件的重复渲染,参数为一个函数和可选的依赖项数组,返回出入函数的记忆版本

基础用法

  • useCallback的用法与useState的用法基本一致,但最后会返回一个函数,用一个变量保存起来。
  • 返回的函数a会根据b的变化而变化,如果b始终未发生变化,a也不会重新生成,避免函数在不必要的情况下更新。
  • 记得子组件导出时使用memo包裹一下,其作用是对组件前后两次进行浅对比,阻止不必要的更新。
const a = useCallback(() => {
   xxx...
},[b])
// Parent.jsx
import React, { useState } from "react";
import { Button } from "antd";
import Child from "./Child";

function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log("父組件重新渲染=============");

  const computedFn = () => {
    return parentCount + 1;
  };

  return (
    <div style={{width: '300px',backgroundColor: '#ccc'}}>
      <Child computedValue={parentCount} computedFn={computedFn} />
      <Button
        type="primary"
        onClick={() => {
          setParentCount(parentCount + 1);
        }}
        style={{ marginRight: "10px" }}
      >
        父组件 +1
      </Button>

      <Button
        type="primary"
        onClick={() => {
          setOtherCount(otherCount + 1);
        }}
      >
        父组件 otherCount+1
      </Button>
    </div>
  );
}
export default Parent;

// Child.jsx
import React from 'react';
import { Button } from 'antd';

function Child(props) {
  // props解构
  const { 
    computedValue, 
    computedFn,
  } = props;
  console.log('=============子組件重新渲染');
  return (
    <div style={{width: '300px', height: '100px', backgroundColor: '#ccc'}}>
      <Button type="primary" onClick={computedFn}>子組件</Button>
      <div>
        父组件传入的计算结果:
        {computedValue}
      </div>
      
    </div>
  );
}
export default React.memo(Child);

当点击第二个父组件 otherCount+1按钮时,otherCount子组件props无关,子组件也会重新渲染。

效果

image.png

image.png

使用usecallback,给computedFn 加上useCallBack

  const computedFn = useCallback(() => {
    return parentCount + 1;
  },[parentCount]);

再点击父组件第二个父组件 otherCount+1按钮,子组件不会重新渲染,因为useCallback的依赖项parentCount没变更,返回的是上一次渲染的函数,因此传入子组件的props没变,组件不会重新渲染。

  • 需要注意的是,被useCallback保存的函数内部作用域也不会变更,因此,当依赖项数组为空的时候,传入useCallback的函数的内部通过闭包取的组件内的变量值终不变。
// Parent.jsx
import React, { useState, useCallback } from "react";
import { Button } from "antd";
import Child from "./Child";

let a = 0;
function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log("父組件重新渲染=============");

  const computedFn = useCallback(() => {
    // 依赖项为空,这里的打印值始终不变;
    // 因为组件state变化时会重新渲染整个组件,而这里parentCount取的始终是第一次渲染版本的值
    console.log(parentCount); 
    // 这里的打印值会实时更新,因为变量直接定义在组件外部,不受组件重新渲染影响
    console.log(a);
    return parentCount + 1;
  }, []);

  return (
    <div style={{width: '300px',backgroundColor: '#ccc'}}>
      <Child computedValue={parentCount} computedFn={computedFn} />
      <Button
        type="primary"
        onClick={() => {
          setParentCount(parentCount + 1);
          a += 1;
        }}
        style={{ marginRight: "10px" }}
      >
        父组件 +1
      </Button>

      <Button
        type="primary"
        onClick={() => {
          setOtherCount(otherCount + 1);
        }}
      >
        父组件 otherCount+1
      </Button>
    </div>
  );
}

export default Parent;
  • 因为useCallback目的是减少子组件重渲染,因此需要搭配子组件的shouldComponentUpdateReact.memo 一起使用才有优化意义。

  • 以上是依赖项变更不频繁的情况,当依赖项变更频繁时,useCallback的记忆效果就不好,可以使用ref 作为依赖项解决。

// Parent.jsx
import React, { useState, useCallback, useEffect, useRef } from "react";
import Child  from "./Child";

function Form() {
  const [text, setText] = useState('');
  const textRef = useRef();
  console.log("Form组件渲染");

  useEffect(() => {
    textRef.current = text; // 把它写入 ref
  },[text]);

  const handleSubmit = useCallback(() => {
    // ref 对象在组件的整个生命周期内保持不变
    // 从 ref 读取它,current的变更不会引起组件的重新渲染,而函数内部又能拿到正确的值
    const currentText = textRef.current; 
    console.log("currentText",currentText);
  }, [textRef]);

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Child handleSubmit={handleSubmit}>提交</Child>
    </>
  );
}
export default Form;

// Child.jsx
import React from 'react';
import { Button } from 'antd';

function Child(props) {
  // props解构
  const { 
    handleSubmit,
  } = props;

  console.log('=============子組件重新渲染');
  return (
    <>
      <Button type="primary" onClick={handleSubmit}>子組件</Button>
    </>
  );
}
export default React.memo(Child);

效果

组件挂载:

image.png

text变更不会重新渲染子组件:

image.png

扩展--useRef

官方:

const refContainer = useRef(initialValue);
  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

  • useRef创建的对象有个current 属性,这个属性就像个盒子,啥都能存,包括DOM节点;返回的 ref 对象在组件的整个生命周期内保持不变,即存在current的值不受组件重新渲染影响,始终保持着一开始的引用;同时该属性的变更也不会触发组件的重新渲染;这个属性的初始值为useRef的参数。

官方例子:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

当把useRef 创建的对象传给DOM 元素的ref属性时,react会把当前DOM元素的引用存入current属性,这样就可以通过ref对象直接操作DOM元素了。

2.useMemo

简单来说就是传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。

useMemo 与 useCallback 很像,根据上述 useCallback 已经可以想到 useMemo 也能针对传入子组件的值进行缓存优化。

// ...
const [count, setCount] = useState(0);

const userInfo = {
  // ...
  age: count,
  name: 'Jace'
}

return <UserCard userInfo={userInfo}>
// ...
const [count, setCount] = useState(0);

const userInfo = useMemo(() => {
  return {
    // ...
    name: "Jace",
    age: count
  };
}, [count]);

return <UserCard userInfo={userInfo}>

很明显的上面的 userInfo 每次都将是一个新的对象,无论 count 发生改变没,都会导致 UserCard 重新渲染,而下面的则会在 count 改变后才会返回新的对象。

上述用法是有有关于父子组件传值带来的性能优化,实际上 useMemo 的作用不止于此,根据官方文档内介绍:

  • 可以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新。
const num = useMemo(() => {
  let num = 0;
  // 这里使用 count 针对 num 做一些很复杂的计算,
  // 当 count 没改变的时候,组件重新渲染就会直接返回之前缓存的值。
  return num;
}, [count]);

return <div>{num}</div>