useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return (
<div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count +
1)}> +c1 </button>
<input value={val} onChange={event =>
setValue(event.target.value)}/>
</div>
</div>);
}
useCallback
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return (
<div>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+
</button>
<input value={val}
onChange={event => setVal(event.target.value)}/>
</div>
</div>);
}
我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。
知道useCallback有什么样的特点,那有什么作用呢?
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return (
<div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+
</button>
<input value={val} onChange={event =>
setVal(event.target.value)}/>
</div>
</div>);
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return (<div> {count} </div>)
}
不仅是上面的例子,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。
useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。
useRef
useRef hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据
它保存数据的用法
在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state在多次渲染之后依旧不变。但是的,state问题在于一旦修改了它就会造成组件的重新渲染。
那么这个时候就可以使useRef用来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const timerID = useRef();
useEffect(() => {
timerID.current = setInterval(()=>{
setCount(count => count + 1);
}, 1000);
}, []);
useEffect(()=>{
if(count > 10){
clearInterval(timerID.current);
}
});
return (
<>
<button ref={couterRef} onClick={() => {setCount(count
+ 1)}}>
Count: {count}, double: {doubleCount}
</button>
</>
);
}
// useRef 穿透闭包
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
// useLayoutEffect: DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
useLayoutEffect(() => {
textRef.current = text; // 将 text 写入到 ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // 从 ref 中读取 text
alert(currentText);
}, [textRef]); // handleSubmit 只会依赖 textRef 的变化。不会在 text 改变时更新
return (
<>
<input value={text} onChange={e =>
updateText(e.target.value)}
/>
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
上面 input 值改变了, handleSubmit 也获取到的值也会变, 但 handleSubmit 不会变,不会触发ExpensiveTree 更新,