useCallback/useMemo在React中做什么



如文档中所述,useCallback返回一个已存储的回调。

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

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

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

附言:我认为代码笔示例的可视化将帮助每个人更好地理解它。在文档中解释。

当您希望防止不必要的重新渲染以获得更好的性能时,最好使用此选项。

比较这两种将回调传递到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.memo实现Button,它将始终重新渲染(除非您有一些自定义比较功能),因为onClick道具每次都不同,而在第二种情况下则不会。

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

在渲染方法中使用箭头函数可以吗?一般来说,是的,这是可以的,而且它通常是将参数传递给回调函数

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

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>
}

请注意,handleClick-function实例将在块内的每个函数调用上创建,因此每个调用上的事件处理程序地址将不同。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>
}

现在,使用上面的代码,React将通过useCallback-函数调用将handleClick-事件处理程序标识为相同的。它将始终返回相同的函数实例,React组件渲染机制将很高兴。

通过useCallback在内部存储函数将导致一个新问题。函数调用的存储实例将无法直接访问当前函数调用的变量。相反,它将看到在创建存储函数的初始闭包调用中引入的变量。因此,该调用将不适用于更新后的变量。这就是为什么你需要知道一些使用的变量是否发生了变化。以便useCallback将当前函数调用实例存储为新存储的实例。作为useCallback的第二个参数的变量列表列出了此功能的变量。在我们的例子中,我们需要告诉useCallback-函数,我们需要在每次调用中都有一个新版本的计数器变量。如果我们不这样做,调用后的计数器值将始终为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来模拟类的基本功能。从这个意义上说,函数式编程是历史上向过程式编程迈出的一大步。

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

新React钩子方法的这一部分无疑是系统中最薄弱的部分。useCallback非常违反直觉,而且非常容易出错。有了useCallback调用和依赖关系,很容易陷入内部循环。React Class方法没有这个警告。

毕竟,最初的类方法更有效。useCallback将减少重新发送的需要,但每次当其某些因变量发生变化时,它都会重新生成函数,如果变量发生变化,则匹配本身将产生开销。这可能会导致多余的重新应答。React类的情况并非如此。

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

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'));

默认情况下,每次渲染都会重新创建事件处理程序并为其分配不同的地址,从而导致"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"));

useCallback→如果在父组件内呈现多个组件,并且事件处理程序作为CCD_。如果在父组件中对状态进行了任何更新,它将重新呈现父组件,尽管子组件不依赖于更新的状态。因此,这使得额外的重新渲染成为性能问题。

为了解决这些问题,我们可以缓存函数本身的useCallback钩子,因为组件的每个呈现都会创建函数的新实例。

在下面的例子中,计数器组件的切换按钮的onclick每次都在重新渲染,所以我们使用回调挂钩来停止重新渲染

import React, {useState, useEffect, useCallback} from 'react';

const IncrementCounter = (props) => {
useEffect(() => {
console.log('Counter Function Called');
}, [props.incrementVal]);
return (
<div>
<button onClick={() => props.incrementVal(1)}>Increment Counter</button>
<p>Counter Val: {props.counter}</p>
</div>
)
}

function Question2CallBack(props) {
const [toggle, setToggle] = useState(false);
const [counter, setCounter] = useState(0);

// USING USECALLBACK FOR CATCHING THE SAME REFRENCE OF FUNCTION WHILE EACH RENDER DURING STATE CHANGE
const incrementVal = useCallback((val) => {
setCounter(()=> counter + val);
}, [counter]);

return (
<div>
<button onClick={() => setToggle(!toggle)}>Toggle</button>
<p>Toggle Value: {toggle ? 'true': 'false'}</p>
<IncrementCounter incrementVal={incrementVal} counter={counter}/>
</div>
);
}

export default Question2CallBack;

相关内容

  • 没有找到相关文章

最新更新