React.useCallback()介绍及应用

1,145 阅读4分钟

我博客的一位读者在Facebook上向我提出了一个有趣的问题。他说他的队友们,无论在什么情况下,都是把每个回调函数包裹在useCallback()

import React, { useCallback } from 'react';
function MyComponent() {
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);
  return <MyChild onClick={handleClick} />;
}

"每个回调函数都应该被备忘录化,以防止对使用该回调函数的子组件进行无用的重新渲染",这是他的队友们的推论。

这种推理与事实相去甚远。这种没有经过分析的useCallback() ,会使组件的速度变慢。

在这篇文章中,我将解释如何正确使用useCallback()

1.了解函数的平等性检查

在深入了解useCallback() 的用法之前,让我们先区分一下useCallback() 所解决的问题--函数平等检查。

JavaScript中的函数是一等公民,也就是说,一个函数是一个普通的对象。函数对象可以被其他函数返回,可以被比较,等等:任何你能对一个对象做的事情。

让我们写一个函数factory() ,它可以返回对数字求和的函数。

function factory() {
  return (a, b) => a + b;
}
const sum1 = factory();
const sum2 = factory();
sum1(1, 2); // => 3
sum2(1, 2); // => 3
sum1 === sum2; // => false
sum1 === sum1; // => true

函数sum1sum2 共享相同的代码源,但它们是不同的函数对象。比较它们sum1 === sum2 ,评估为false

这就是JavaScript对象的工作方式。一个对象(包括一个函数对象)只等于它自己。

2.useCallback()的目的

共享相同代码的不同函数对象经常在React组件内部创建。

function MyComponent() {
  // handleClick is re-created on each render
  const handleClick = () => {
    console.log('Clicked!');
  };
  // ...
}

handleClick 是在每次渲染 ,MyComponent是一个不同的函数对象。

因为内联函数很便宜,所以在每次渲染上重新创建函数并不是什么问题。每个组件有几个内联函数是可以接受的。

但在某些情况下,你需要在不同的渲染中保持一个单一的函数实例:

  1. 一个被包裹在React.memo() 内的功能组件接受一个函数对象道具。
  2. 当函数对象是对其他钩子的依赖时,例如useEffect(..., [callback])
  3. 当函数有一些内部状态时,例如,当函数被放行或被节流时。

这时useCallback(callbackFun, deps) 有所帮助:给定相同的依赖值deps ,钩子在渲染之间返回相同的函数实例(又称记忆化)。

import { useCallback } from 'react';
function MyComponent() {
  // handleClick is the same function object
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  // ...
}

handleClickMyComponent渲染之间,变量总是有相同的回调函数对象。

3.一个好的用例

想象一下,你有一个组件可以渲染一个大的项目列表:

import useSearch from './fetch-items';
function MyBigList({ term, onItemClick }) {
  const items = useSearch(term);
  const map = item => <div onClick={onItemClick}>{item}</div>;
  return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);

这个列表可能很大,可能有几百个项目。为了防止无用的列表重新渲染,你把它包装成React.memo()

MyBigList 的父组件提供了一个处理函数,以了解一个项目被点击的情况。

import { useCallback } from 'react';
export function MyParent({ term }) {
  const onItemClick = useCallback(event => {
    console.log('You clicked ', event.currentTarget);
  }, [term]);
  return (
    <MyBigList
      term={term}
      onItemClick={onItemClick}
    />
  );
}

MyParent 组件重新渲染时,onItemClick 函数对象保持不变,不会破坏MyBigList 的记忆化。

这就是useCallback() 的一个很好的用例。

4.一个坏的用例

让我们看看另一个例子。

import { useCallback } from 'react';
function MyComponent() {
  // Contrived use of `useCallback()`
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);
  return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

应用useCallback() 有意义吗?很可能不是,因为<MyChild> 组件很轻,它的重新渲染不会产生性能问题。

不要忘了,每次MyComponent 渲染时都会调用useCallback() 钩子。即使是useCallback() 返回相同的函数对象,在每次重新渲染时仍然要重新创建内联函数(useCallback() 只是跳过它)。

通过使用useCallback() ,你也增加了代码的复杂性。你必须使useCallback(..., deps)deps 与你在memoized回调中使用的内容保持同步。

总而言之,优化的代价比没有优化的代价要大

只要接受渲染会产生新的函数对象。

import { useCallback } from 'react';
function MyComponent() {
  const handleClick = () => {
    // handle the click event
  };
  return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

5.总结

在考虑性能调整的时候,请记住这句话

在优化之前先进行剖析

当决定使用一种优化技术时,包括记忆化,特别是useCallback() ,要做到:

  • 首先 -剖析
  • 然后量化所增加的性能(例如:150ms50ms 渲染速度的增加)。

然后问自己:与增加的复杂性相比,增加的性能是否值得使用useCallback()