React 基础巩固(四十五)——其他Hooks(useMemo、useRef、useImperativeHandle、useLayoutEffect)
一、useMemo的使用
useCallback会返回一个函数的memoized(记忆的)值,其目的是不希望子组件进行多次渲染,并非为了函数进行缓存。
反观useMemo,useMemo返回的也是一个memoized(记忆的)值,这个值是函数返回结果的值,其目的也是为了进行性能的优化。
// useMemo、useCallback 对比:一个是对函数做优化,一个是对函数返回结果做优化
const increment = useCallback(fn, []);
const increment2 = useMemo(() => fn, []);
useMemo的使用:
import React, {
memo, useCallback, useMemo, useState } from "react";
function calcNumTotal(num) {
console.log("calcNumTotal的计算过程被调用~");
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return total;
}
export default memo(function App() {
const [count, setCount] = useState(0);
const result = useMemo(() => {
return calcNumTotal(50 * 2);
}, [count]);
// useMemo、useCallback 对比:一个是对函数做优化,一个是对函数返回结果做优化
// const increment = useCallback(fn, []);
// const increment2 = useMemo(() => fn, []);
return (
<div>
<h2>计算结果:{
result}</h2>
<h2>计数器:{
count}</h2>
<button onClick={
(e) => setCount(count + 1)}>+1</button>
</div>
);
});
二、useRef的使用
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期中保持不变。
用法一:绑定dom元素
import React, {
memo, useRef } from "react";
export default memo(function App() {
const titleRef = useRef();
const inputRef = useRef();
function showTitleDom() {
console.log(titleRef.current);
inputRef.current.focus();
}
return (
<div>
<h2 ref={
titleRef} className="title">
hello world
</h2>
<input type="text" ref={
inputRef} />
<button onClick={
showTitleDom}>查看title的dom</button>
</div>
);
});
用法二:绑定值(解决闭包陷阱问题)
保存一个数据,这个对象在整个生命周期中可以保持不变。
import React, {
memo, useCallback, useRef, useState } from "react";
let obj = null;
export default memo(function App() {
const [count, setCount] = useState(0);
const nameRef = useRef();
// 验证:判断是否为同一对象
console.log(obj === nameRef);
obj = nameRef;
// 通过useRef解决闭包陷阱
const countRef = useRef();
countRef.current = count;
const increment = useCallback(() => {
setCount(countRef.current + 1);
}, []);
return (
<div>
<h2>hello world: {
count}</h2>
<button onClick={
(e) => setCount(count + 1)}>+1</button>
<button onClick={
increment}>+1</button>
</div>
);
});
三、useImperativeHandle的使用
useImperativeHandle可以避免直接将子组件的DOM返回给父组件,因为直接暴露给父组件可能带来不可控的问题,父组件可以利用DOM进行任意的操作,这是我们不希望看到的。
通过useImperativeHandle我们可以控制子组件DOM暴露给父组件的操作,例如暴露focus()方法时,在父组件中就只能够通过focus()进行聚焦,不能进行其他的操作;暴露setValue()方法时,在父组件中就只能狗通过setValue()进行value修改,不能进行其他的操作。
import React, {
forwardRef, memo, useImperativeHandle, useRef } from "react";
const HelloWorld = memo(
forwardRef((props, ref) => {
const inputRef = useRef();
// 子组件对父组件传入的ref进行处理
useImperativeHandle(ref, () => {
return {
focus() {
console.log("focus");
inputRef.current.focus();
},
setValue(value) {
inputRef.current.value = value;
},
};
});
return <input ref={
inputRef} type="text" />;
})
);
export default memo(function App() {
const titleRef = useRef();
const inputRef = useRef();
function handleDom() {
console.log(titleRef.current);
console.log(inputRef.current);
inputRef.current.focus();
inputRef.current.setValue("666");
}
return (
<div>
<h2 ref={
titleRef}>hello world</h2>
<HelloWorld ref={
inputRef} />
<button onClick={
handleDom}>DOM操作</button>
</div>
);
});
四、useLayoutEffect的使用
useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;而useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新。
当我们希望某些操作发生之后再更新DOM,可以将操作放在useLayoutEffect中:
import React, {
memo, useEffect, useLayoutEffect, useState } from "react";
export default memo(function App() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log("useLayoutEffect");
});
useEffect(() => {
console.log("useEffect");
});
return (
<div>
<h2>{
count}</h2>
<button onClick={
(e) => setCount(count + 1)}>+ 1</button>
</div>
);
});
当数据准备渲染到页面上之前,发现数据不对,能够及时进行调整。例如:点击按钮,将count置为0。当发现count为0时,以随机数替换处理。
我们尝试用useEffect来实现:
import React, {
memo, useEffect, useState } from "react";
export default memo(function App() {
const [count, setCount] = useState(100);
useEffect(() => {
console.log("useEffect");
if (count === 0) {
setCount(Math.random() + 99);
}
});
return (
<div>
<h2>count: {
count}</h2>
<button onClick={
(e) => setCount(0)}>设置为 0</button>
</div>
);
});
因为useEffect是在渲染后执行,所以界面上会在短时间内先显示0,再显示我们想要取代0的随机数,从而会造成闪烁现象。
我们再尝试用useLayoutEffect来实现:
useLayoutEffect(() => {
console.log("useEffect");
if (count === 0) {
setCount(Math.random() + 99);
}
});
此时,count在渲染之前就被发现为0,并替换为随机数,随后渲染。此时,则避免了闪烁现象。