useCallback/useMemo 在 React 中做什么?

What does useCallback/useMemo do in React?

docsuseCallback 中所述 Returns 记忆回调。

传递内联回调和输入数组。 useCallback 将 return 一个记忆版本的回调,只有当其中一个输入发生变化时才会发生变化。这在将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染时很有用(例如 shouldComponentUpdate)。

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

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

P.S。我认为可视化 codepen example will help everyone to understand it better. Explained in docs.

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

比较这两种将回调传递给取自 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 会为您记住回调函数,并且不会多次创建回调。

因此,在第一种情况下,如果 Button 是使用 React.memo 实现的,它将始终重新渲染(除非您有一些自定义比较功能),因为 onClick 道具每次都不同,在第二种情况下,它不会。

在大多数情况下,采用第一种方式就可以了。正如 React 文档所述:

Is it OK to use arrow functions in render methods? Generally speaking, yes, it is OK, and it is often the easiest way to pass parameters to callback functions.

If you do have performance issues, by all means, optimize!

我做了一个小例子来帮助其他人更好地理解它的行为方式。您可以 运行 演示 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'));

useCallbackuseMemo 试图绕过 React hooks 选择的函数式编程方法带来的弱点。在 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>
}

请注意,将在块内的每个函数调用时创建 handleClick -函数实例,因此每次调用的事件处理程序地址将不同。因此,React 框架将始终将事件处理程序视为已更改。在上面的示例中,React 会将 handleClick 视为每次调用的新值。它根本没有工具可以将其识别为同一个调用。

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

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

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

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

现在,使用上面的代码,React 将识别 handleClick -事件处理程序相同,这要归功于 useCallback - 函数调用。它总是 return 相同的函数实例和 React 组件渲染机制会很高兴。

通过 useCallback 内部存储函数将导致新问题。函数调用的存储实例将无法直接访问当前函数调用的变量。相反,它将看到在创建存储函数的初始闭包调用中引入的变量。因此,该调用不适用于更新的变量。这就是为什么您需要判断某些使用的变量是否已更改的原因。这样 useCallback 会将当前函数调用实例存储为新的存储实例。作为 useCallback 的第二个参数的变量列表列出了此功能的变量。在我们的示例中,我们需要告诉 useCallback -函数我们需要在每次调用时使用新版本的 counter -变量。如果我们不这样做,调用后的计数器值将始终为 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 -call 在这里只是出于同样的原因。功能块没有内部状态,因此挂钩使用 useStateuseCallbackuseMemo 来模仿 类 的基本功能。从这个意义上说,函数式编程在历史上向过程式编程迈出了一大步。

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

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

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

不是这种情况

默认情况下,事件处理程序会在每次渲染时重新创建并分配不同的地址,从而导致“props”对象发生变化。下面,按钮 2 没有重复渲染,因为“props”对象没有改变。请注意整个 Example() 函数如何在每次渲染时运行直至完成。

const MyButton = React.memo(props=>{
   console.log('firing from '+props.id);
   return (<button onClick={props.eh}>{props.id}</button>);
});

function Example(){
   const [a,setA] = React.useState(0);
   const unmemoizedCallback = () => {};
   const memoizedCallback = React.useCallback(()=>{},[]);   // don’t forget []!
   setTimeout(()=>{setA(a=>(a+1));},3000);
   return (<React.Fragment>
                 <MyButton id="1" eh={unmemoizedCallback}/>
                 <MyButton id="2" eh={memoizedCallback}/>
                 <MyButton id="3" eh={()=>memoizedCallback}/>
           </React.Fragment>);
} 
ReactDOM.render(<Example/>,document.querySelector("div"));