Skip to content

React Hook系列(三)—— useMemo、useCallback #66

@LightXJ

Description

@LightXJ

useMemo

useMemo作用
常常用于缓存一些复杂计算的结果。useMemo 接收一个函数和依赖数组,当数组中依赖项变化的时候,这个函数就会执行,返回新的值。【对象做依赖的话,进行的是浅比较,也就是对比两个对象的引用是否相同】

const sum = useMemo(() => {    
    // 一系列计算
}, [count])

举个例子会更加清楚 useMemo 的使用场景,我们就以下面这个 DatePicker 组件的计算为例:
image
DatePicker 组件每次打开或者切换月份的时候,都需要大量的计算来算出当前需要展示哪些日期。然后再将计算后的结果渲染到单元格里面,这里可以使用 useMemo 来缓存,只有当传入的日期变化时才去计算。

举例:

function Child(props) {
  console.log('Child render');
  return (
    <p>
      name is
      {props.data.name}
    </p>
  );
}

export default function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Mary');
  console.log('Parent render');
  const data = { name };
  return (
    <div>
      <p>
        click
        {' '}
        {count}
        {' '}
        times
      </p>
      <Child data={data} />

      <button type="button" onClick={() => { setCount(count + 1); }}>+</button>
      <button type="button" onClick={() => { setName(`${Date.now()}`); }}>change Name</button>
    </div>
  );
}

上述这个例子的执行结果为,点击+按钮或者change Name按钮,Child都会渲染,输出Child render
但是我们希望Child的render只依赖于它自己的props,不受其他影响,那么我们可以使用React.memo实现(React.memo 仅检查 props 变更)。我们改写例子如下

function Child(props) {
  console.log('Child render');
  return (
    <p>
      name is
      {props.data.name}
    </p>
  );
}

Child = memo(Child);

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Mary');
  console.log('Parent render');

  const data = { name };
  return (
    <div>
      <p>
        click
        {' '}
        {count}
        {' '}
        times
      </p>
      <Child data={data} />

      <button type="button" onClick={() => { setCount(count + 1); }}>+</button>
      <button type="button" onClick={() => { setName(`${Date.now()}`); }}>change Name</button>
    </div>
  );
}

但是我们发现,这样修改后,运行结果仍然没变,即使name没有变化,父组件count变化后,Child还是render了,原因是setCount后,整个App函数都重新执行,data = { name } 相当于重新生成了一个新的对象,所以Child还是render了。
解决这个问题的一个解决方案就是上面提到的:useMemo

const data = useMemo(() => ({ name }), [name]);

这样,我们发现光是count变化后,子组件就不渲染了
但是也要注意,要是依赖项为空,即使name变化,那么data也永远不变,Child也不渲染

const data = useMemo(() => ({ name }), []);

useMemo实现原理

比较本次和上次依赖项的值,如果有一个变化,则重新计算callback的值并返回,如果未变化,则返回上次callback的值

let lastMemoDeps = [];
let lastMemoCallback = '';
function useMemo(callback, deps) {
  if (lastMemoDeps) {
    const changed = deps.some((item, index) => {
      if (item !== lastMemoDeps[index]) {
        return true;
      }
      return false;
    });
    if (changed) {
      lastMemoDeps = deps;
      lastMemoCallback = callback();
      return lastMemoCallback;
    }
  } else {
    lastMemoDeps = deps;
    lastMemoCallback = callback();
  }
  return lastMemoCallback;
}

useCallback

和 useMemo 类似,只不过 useCallback 是用来缓存函数。
匿名函数导致不必要的渲染
在我们编写 React 组件的时候,经常会用到事件处理函数,很多人都会简单粗暴的传一个箭头函数。

class App extends Component {
    render() {
        return <h1 onClick={() => {}}></h1>
    }
}

这种箭头函数有个问题,那就是在每一次组件重新渲染的时候都会生成一个重复的匿名箭头函数,导致传给组件的参数发生了变化,对性能造成一定的损耗。
在函数组件里面,同样会有这个传递新的匿名函数的问题。从下面这个例子来看,每次点击 change Name,就会导致Child渲染,通过useMemo我们已经知道,data不会变化,但是addClick每次在父组件渲染后都会生成一个新的方法,所以Child也跟着渲染了。

function Child(props) {
  console.log('Child render');
  return (
    <button type="button" onClick={props.addClick}>{props.data.count}</button>
  );
}

Child = memo(Child);

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Mary');

  const data = useMemo(() => ({ count }), [count]);

  const addClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>
        name is :
        {' '}
        {name}
      </p>
      <Child data={data} addClick={addClick} />
      <button type="button" onClick={() => { setName(`${Date.now()}`); }}>change Name</button>
    </div>
  );
}

这就是体现 useCallback 价值的地方了,我们可以用 useCallback 指定依赖项。在无关更新之后,通过 useCallback 取的还是上一次缓存起来的函数。因此,useCallback 常常配合 React.memo 来一起使用,用于进行性能优化。

const addClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

useCallback实现原理

let lastCallbackDeps = [];
let lastCallback = '';
function useCallback(callback, deps) {
  if (lastCallbackDeps) {
    const changed = deps.some((item, index) => {
      if (item !== lastCallbackDeps[index]) {
        return true;
      }
      return false;
    });
    if (changed) {
      lastCallbackDeps = deps;
      lastCallback = callback;
      return lastCallback;
    }
  } else {
    lastCallbackDeps = deps;
    lastCallback = callback;
  }
  return lastCallback;
}

参考:
1、React Hooks第一期:聊聊useCallback
https://zhuanlan.zhihu.com/p/56975681
2、手写实现react-hook:https://www.acfun.cn/v/ac16764934

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions