useCallback / useMemo在React中做什么?

时间:2018-11-05 17:27:37

标签: javascript reactjs react-hooks

docs中所述,useCallback 返回备注的回调。

传递内联回调和输入数组。 useCallback将返回回显的回调版本,仅在输入之一发生更改时才会更改。在将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染(例如,shouldComponentUpdate)时,这很有用。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

但是它是如何工作的,在React中最好用在哪里?

3 个答案:

答案 0 :(得分:55)

当您要防止不必要的重新渲染以提高性能时,最好使用此方法。

比较将回调传递到从React Docs获取的子组件的以下两种方法:

1。渲染中的箭头功能

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={() => this.handleClick()}>Click Me</Button>;
  }
}

2。在构造函数中绑定(ES2015)

class Foo extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={this.handleClick}>Click Me</Button>;
  }
}

假设<Button>被实现为PureComponent,则第一种方法将导致<Button>每次重新渲染时都重新渲染<Foo>,因为在其中创建了新功能每个render()呼叫。第二种方法,handleClick方法仅在<Foo>的构造函数中创建一次,并在各个渲染器之间重复使用。

如果我们使用挂钩将两种方法都转换为功能组件,则它们是等效的(某种):

1。渲染中的箭头功能->未存储的回调

function Foo() {
  const handleClick = () => {
    console.log('Click happened');
  }
  return <Button onClick={handleClick}>Click Me</Button>;
}

2。绑定到构造函数中(ES2015)->记忆化回调

function Foo() {
  const memoizedHandleClick = useCallback(
    () => console.log('Click happened'), [],
  ); // Tells React to memoize regardless of arguments.
  return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}

第一种方法是在每次调用功能组件时创建回调,但是第二种方法是,React为您记住回调函数,并且不会多次创建该回调。

在大多数情况下,第一种方法很好。如React docs所述:

  

可以在渲染方法中使用箭头功能吗?一般来说,   是的,没关系,并且通常是将参数传递给的最简单方法   回调函数。

     

如果确实存在性能问题,请进行优化!

答案 1 :(得分:3)

我举了一个小例子来帮助其他人更好地理解它的行为。您可以运行演示here或阅读下面的代码:

import React, { useState, useCallback, useMemo } from 'react';
import { render } from 'react-dom';

const App = () => {
    const [state, changeState] = useState({});
    const memoizedValue = useMemo(() => Math.random(), []);
    const memoizedCallback = useCallback(() => console.log(memoizedValue), []);
    const unMemoizedCallback = () => console.log(memoizedValue);
    const {prevMemoizedCallback, prevUnMemoizedCallback} = state;
    return (
      <>
        <p>Memoized value: {memoizedValue}</p>
        <p>New update {Math.random()}</p>
        <p>is prevMemoizedCallback === to memoizedCallback: { String(prevMemoizedCallback === memoizedCallback)}</p>
        <p>is prevUnMemoizedCallback === to unMemoizedCallback: { String(prevUnMemoizedCallback === unMemoizedCallback) }</p>
        <p><button onClick={memoizedCallback}>memoizedCallback</button></p>
        <p><button onClick={unMemoizedCallback}>unMemoizedCallback</button></p>
        <p><button onClick={() => changeState({ prevMemoizedCallback: memoizedCallback, prevUnMemoizedCallback: unMemoizedCallback })}>update State</button></p>
      </>
    );
};

render(<App />, document.getElementById('root'));

答案 2 :(得分:2)

useCallbackuseMemo 试图绕过 React 钩子选择的函数式编程方法带来的弱点。在 Javascript 中,每个实体,无论是函数、变量还是其他任何东西,都会在执行进入函数的代码块时创建到内存中。对于将尝试检测组件是否需要渲染的 React 来说,这是一个大问题。根据输入道具和上下文来扣除重新渲染的需要。让我们看一个带有 useCallback 的简单示例。

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = () => {
    setCounter(counter + 1);
  }

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}

请注意,函数会在每次调用时再次在块内创建每个函数调用,因此每次调用时事件处理程序的地址都会不同。由于这个原因,React 框架将始终看到事件处理程序发生了变化。在上面的示例中,React 会将 handleClick 视为每次调用时的新值。它根本没有工具将其区分为同一个调用。

useCallback 的作用是,如果列出的变量没有改变,它会在内部存储函数的第一个引入版本并将其返回给调用者。

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [])

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}

现在,有了上面的代码,由于 handleClick 函数调用,Reach 将识别 useCallback 事件处理程序。它将始终返回相同的函数实例,React 组件渲染机制会很高兴。

通过 useCallback 在内部存储函数最终会出现新问题。函数调用的存储实例将无法直接访问当前函数调用的变量。相反,它将使用其启动时间的变量值的副本。现在,如果您愿意,调用现在可以用于更新的变量,而不是告诉哪些变量已经替换了原始函数调用。作为 useCallback 的第二个参数的变量列表列出了此功能的变量。在我们的示例中,我们需要告诉 useCallback -function 我们需要在每次调用时都有一个新版本的 counter -variable。如果我们不这样做,调用后的计数器值将始终为 1,它来自原始值 0 加 1。

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [counter])

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}

现在我们有了一个不会在每次调用时都重新渲染的代码的工作版本。

注意到 useState 调用在这里是出于同样的原因。功能块没有内部状态,因此钩子使用 useStateuseCallbackuseMemo 来模仿类的基本功能。从这个意义上说,函数式编程是历史上更接近过程式编程的一大步。

useMemo 是与 useCallback 相同的机制,但用于其他对象和变量。有了它,您可以限制组件重新渲染的需要,因为如果列出的字段没有更改,useMemo 函数将在每个函数调用中返回相同的值。

新的 Reach hooks 方法的这一部分绝对是系统中最薄弱的地方。 useCallback 非常违反直觉,而且非常容易出错。使用 useCallback 调用和依赖项,很容易改变内部循环。我们在 React Class 方法中没有这个警告。

毕竟原始的类方法更有效。 useCallback 将减少重新渲染的需要,但每次当它的一些因变量发生变化时它都会再次重新生成函数,如果变量发生变化,匹配本身会产生开销。这可能会导致不必要的重新渲染。 React 类不是这种情况。