useLayoutEffect
前面我们对 useEffect 的了解我们知道,useEffect 始终在渲染之后执行,因此 useEffect 可以访问渲染后的所有值,但是我们不难发现,这其实是有执行顺序的,也就是,效应始终在渲染后执行,在此,我们即将解读一个新的钩子——useLayoutEffect。
useLayoutEffect 与 useEffect 有相似之处,就是都是在组件渲染后执行,同样可以返回一个函数,这个函数在组件移除时调用。但是,它们究竟谁先执行,谁后执行呢?我们一起来看一看这个例子,就一目了然了。
import { useEffect, useLayoutEffect } from "react";
const Example = () => {
useEffect(() => {
console.log("useEffect 执行了!");
});
useLayoutEffect(() => {
console.log("useLayoutEffect 执行了!");
});
return (
<p>useEffect 和 useLayoutEffect 谁先执行?</p>
)
}
export default Example;
结果会是什么呢?
虽然第一个钩子是 useEffect,第二个钩子是 useLayoutEffect,但是从控制台打印的结果我们不难看出,useLayoutEffect 钩子在组件渲染后先执行,而 useEffect 后执行。就此我们来看一下,组件渲染后的一个执行顺序。
我们从上图可以看出,useLayoutEffect 钩子在组件渲染后就直接回调了,等到浏览器绘制完成才会调用 useEffect 钩子。
import { useEffect, useLayoutEffect, useRef } from "react";
function DrawRect() {
const divRef = useRef();
useLayoutEffect(() => {
var divNode = divRef.current;
console.log("useLayoutEffectSize", divNode.clientWidth, divNode.clientHeight);
divNode.attributes.style.value = `width: ${divNode.clientWidth + 100}px; height: ${divNode.clientHeight + 100}px; border: 1px solid red;`
}, []);
useEffect(() => {
var divNode = divRef.current;
console.log("useEffectSize", divNode.clientWidth, divNode.clientHeight);
}, [])
return (
<>
<div ref={divRef} style={{width: 0, height: 0, border: '1px solid red'}}></div>
</>
)
}
export default DrawRect;
从上面代码我们可以看到,我们初始化的 <div> 的宽高是0,当渲染完成后,我们在 useLayoutEffect 钩子中可以拿到宽高属性也为0,同时我们在这个阶段把元素样式进行重新设置,结果在 useEffect 中我们拿到的元素宽高为100,最终浏览器绘制出来的 <div> 我们也能看到宽高为100。这个案例印证了我们上面提供的组件渲染后的钩子执行顺序。
通过上面的示例是不是也给我们提供了一些使用 useLayoutEffect 钩子的思路,我们是不是可以在这个钩子中进行浏览器绘制样式的设定呢?答案是肯定的,我们从 Layout 这个单词也能明白其大意,那肯定是跟布局有关,在日常开发中,这个钩子可能用得并不太多,但是,对于样式的设定,修改是可以再这个钩子中进行操作的。
useMemo
注: "如下代码及图片中 chengedSum,打错了,应该为changedSum,抱歉!"
在我们上一篇文章中了解到了依赖数组可以是同时依赖多个元素,然而同时依赖多个元素的时候,任何一个依赖发生更新,都会使得依赖触发,出现频繁且不是我们需要的方法调用。这个时候 useMemo 钩子就派上用场了,useMemo 会调用一个函数来计算得到一个备忘值,存储在缓存中。这样我们每次再使用时,就不需要再频繁的调用计算来获取结果,所以但凡涉及到缓存的,几乎都是为了提升性能,既然提了提升性能,那么在性能优化时,这就是一个优化点咯哦。我们具体来看一看 useMemo 的语法。
const result = useMemo(() => calculatedResult, [dependence]);
从语法我们可以看到,useMemo 接收两个参数,一个函数且这个函数返回计算结果,useMemo 返回这个函数的计算结果,和一个依赖数组。
import { useEffect, useMemo, useState } from "react";
const SumInput = () => {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const sum = useMemo(() => {
console.log("num1:", num1);
console.log("num2:", num2);
return num1 * 1 + num2 * 1;
}, [num1, num2]);
const exchange = () => {
console.log("点击切换-------");
setNum1(num2);
setNum2(num1);
};
useEffect(() => {
console.log("chengedSum:", sum);
console.log("---------------分割线------------------");
}, [sum]);
return(
<>
<div style={{margin: 20}}><input type="number" value={num1} onChange={(e) => setNum1(e.target.value)}/></div>
<div style={{margin: 20}}><input type="number" value={num2} onChange={(e) => setNum2(e.target.value)}/></div>
<button style={{margin: "10px 20px"}} onClick={exchange}>互换</button>
</>
);
};
export default SumInput;
我们看到这个组件是input框值相加的的组件,当组件初始化的时候,chengedSum 会打印出为 0 的值,然后我们分别输入 4 和 5,chengedSum 打印出 9,我们试想一下,如果点击切换按钮,chengedSum 会打印出啥?
我们来看看这个打印过程及点击切换后的结果,发现,当我们点击切换按钮后,chengedSum 并没有打印,也就是 useEffect 依赖的 sum 根本没有发生变化,应为上一次计算的结果也为 9,所以切换以后,计算结果还是为 9,所以就不会再打印该值了。从上图中我们还可以发现,num1 和 num2 中任何一个值发生变化,都会使得 useMemo 重新调用。
上图中虚线部分我模拟了如果 useEffect 直接依赖 num1, num2,那么不管是 num1,还是 num2 发生变化都会触发 useEffect 钩子的回调,如果 useEffect 只关注 num1,num2 的和的值是否发生变化,这就会造成不必要的消耗,所以才有了 useMemo,只要有个依赖发生变化,计算然后存起来,等需要的时候直接取就可以了,也不必每次都要去计算了,所以 useMemo 的主要作用就是减少函数式组件的渲染量,提高渲染性能。
useCallback
useCallback 的作用于 useMemo 类似,不过其备忘的是函数。我们先来看这样一个示例:
import { useEffect, useState } from "react";
const userList = [
{name: "炭烤小橙微辣", age: 18},
{name: "炭烤小橙中辣", age: 20},
{name: "炭烤小橙特辣", age: 30},
{name: "炭烤小橙爆辣", age: 35}
];
const GetUserList = () => {
const [name, setName] = useState();
const [age, setAge] = useState('');
const fetchList = () => {
console.log("GetUserList 调用了!");
return name ? userList.filter(user => user.name === name) : userList;
}
const nameChange = (e) => {
console.log("-------------------");
console.log("切换了 name ");
setName(e.target.value);
};
const ageChange = (e) => {
console.log("-------------------");
console.log("age变了");
setAge(Number(e.target.value));
};
return (
<>
<div style={{display: "flex"}}>
<div style={{margin: 20}}>
<label htmlFor="name">姓名:</label>
<select defaultValue value={name} id="name" onChange={nameChange}>
<option value="" style={{display: "none"}}></option>
{
userList.map((user, index) => (
<option key={index} value={user.name}>{user.name}</option>
))
}
</select>
</div>
<div style={{margin: 20}}>
<label htmlFor="age">期待年龄:</label>
<input id="age" onChange={ageChange} style={{width: 100}}></input>
</div>
</div>
<div style={{margin: 20}}>{`期待的年龄是 ${ age } 岁`}</div>
<div>
<List fetchList={fetchList}></List>
</div>
</>
)
}
const List = ({fetchList}) => {
const [list, setList] = useState([]);
useEffect(() => {
console.log("依赖更新了!");
const fList = fetchList();
console.log("获得的list:", JSON.stringify(fList));
setList(fList);
}, [fetchList]);
return (
<ul>
{list.map((user, index) => (<li key={index}>{`${user.name} ———— ${user.age}岁`}</li>))}
</ul>
)
}
export default GetUserList;
这是一个我们在业务代码开发过程中常遇到的,通过筛选条件获取列表的一个组件。父组件为一个筛选条件查询组件和可以输入期待的人物年龄,子组件为列表组件。我们希望可以通过选择姓名来查询列表,输入的年龄展示在列表上方。
从结果来看:
- 切换 name 选项,name 更新,组件重新渲染,导致 fetchList 依赖更新,获取了列表。
- 输入年龄,期待的年龄也展示了。
从上述结果来看,虽然我们实现了想要的功能,但是,输入年龄后,同样去获取了列表,这就增加了多余的开销,似乎得优化一下。我们来分析一下为啥会出现这种情况:组件的重新渲染,是有销毁再渲染的过程,也就是初始化的 fetchList 与 重渲染后的 fetchList 虽然是一样的名字,一样的功能,但实质已经不是同一个了,最主要的原因就是在存储中的指引反生了改变。所以我决定这样来修改一下:
import React, { useCallback, useEffect, useState } from "react";
const userList = [
{name: "炭烤小橙微辣", age: 18},
{name: "炭烤小橙中辣", age: 20},
{name: "炭烤小橙特辣", age: 30},
{name: "炭烤小橙爆辣", age: 35}
];
const GetUserList = () => {
const [name, setName] = useState();
const [age, setAge] = useState('');
const fetchList = useCallback(() => {
console.log("GetUserList 调用了!");
return name ? userList.filter(user => user.name === name) : userList;
}, [name]);
const nameChange = (e) => {
console.log("-------------------");
console.log("切换了 name ");
setName(e.target.value);
};
const ageChange = (e) => {
console.log("-------------------");
console.log("age变了");
setAge(Number(e.target.value));
};
return (
<>
<div style={{display: "flex"}}>
<div style={{margin: 20}}>
<label htmlFor="name">姓名:</label>
<select defaultValue value={name} id="name" onChange={nameChange}>
<option value="" style={{display: "none"}}></option>
{
userList.map((user, index) => (
<option key={index} value={user.name}>{user.name}</option>
))
}
</select>
</div>
<div style={{margin: 20}}>
<label htmlFor="age">期待年龄:</label>
<input id="age" onChange={ageChange} style={{width: 100}}></input>
</div>
</div>
<div style={{margin: 20}}>{`期待的年龄是 ${ age } 岁`}</div>
<div>
<List fetchList={fetchList}></List>
</div>
</>
)
}
const List = ({fetchList}) => {
const [list, setList] = useState([]);
console.log("子组件更新了!");
useEffect(() => {
console.log("依赖更新了!");
const fList = fetchList();
console.log("获得的list:", JSON.stringify(fList));
setList(fList);
}, [fetchList]);
return (
<ul>
{list.map((user, index) => (<li key={index}>{`${user.name} ———— ${user.age}岁`}</li>))}
</ul>
)
}
export default GetUserList;
我们使用了 useCallback 来包装 fetchList,当我们再次去输入年龄时,就不会再去查询列表了。解决完一个问题,又来一个问题,我们输入年龄的时候,子组件重新渲染了,但是我们知道年龄的变化跟子组件半毛钱关系都没有,所以不想让子组件重新刷新,看来这个地方还是不够完善啊。
看来我们还得处理一下,子组件添加 React.memo 方法进行包裹。
import { useCallback, useEffect, useState, memo } from "react";
const userList = [
{name: "炭烤小橙微辣", age: 18},
{name: "炭烤小橙中辣", age: 20},
{name: "炭烤小橙特辣", age: 30},
{name: "炭烤小橙爆辣", age: 35}
];
const GetUserList = () => {
const [name, setName] = useState();
const [age, setAge] = useState('');
const fetchList = useCallback(() => {
console.log("GetUserList 调用了!");
return name ? userList.filter(user => user.name === name) : userList;
}, [name]);
const nameChange = (e) => {
console.log("-------------------");
console.log("切换了 name ");
setName(e.target.value);
};
const ageChange = (e) => {
console.log("-------------------");
console.log("age变了");
setAge(Number(e.target.value));
};
return (
<>
<div style={{display: "flex"}}>
<div style={{margin: 20}}>
<label htmlFor="name">姓名:</label>
<select defaultValue value={name} id="name" onChange={nameChange}>
<option value="" style={{display: "none"}}></option>
{
userList.map((user, index) => (
<option key={index} value={user.name}>{user.name}</option>
))
}
</select>
</div>
<div style={{margin: 20}}>
<label htmlFor="age">期待年龄:</label>
<input id="age" onChange={ageChange} style={{width: 100}}></input>
</div>
</div>
<div style={{margin: 20}}>{`期待的年龄是 ${ age } 岁`}</div>
<div>
<List fetchList={fetchList}></List>
</div>
</>
)
}
const List = memo(({fetchList}) => {
const [list, setList] = useState([]);
console.log("子组件更新了!");
useEffect(() => {
console.log("依赖更新了!");
const fList = fetchList();
console.log("获得的list:", JSON.stringify(fList));
setList(fList);
}, [fetchList]);
return (
<ul>
{list.map((user, index) => (<li key={index}>{`${user.name} ———— ${user.age}岁`}</li>))}
</ul>
)
})
export default GetUserList;
我们还是做上边的操作对结果进行对比:
我们可以看到输入年龄后,子组件不再进行重渲染了。我们顺带来了解一下 React.memo 方法。
memo(Component, arePropsEqual?);
-
Component: 需要进行缓存的组件。
memo方法会返回一个全新,被记忆化(缓存)后的组件。
-
arePropsEqual: 可选参数,类型为函数,接收两个参数:上一个组件的 props 与 当前组件的 props。
arePropsEqual = (preProps, curProps) => preProps === curProps使用时通常不需要添加此参数,React 默认使用 Object.is 比较每个 props,但是这只是一个浅层比较,如果需要对负责对象进行比较,则需要手动添加 arePropsEqual 参数进行比较,为 false 重新渲染,反之不渲染。
-
作用:当 props 没有改变时跳过重新渲染(性能优化点)。
-
注意:默认浅比较、只关注 prop 是否改变,本组件内重新渲染条件照常执行。
useReduce
前面的章节我们已经探讨了 useState 钩子,我们日常的开发工作中,绝大多数时候都是用到的的 useState,但是有一些特殊场景,我们使用 useReducer 就会更友好一些。我们先来看看 useReducer 的语法:
const [state, dispatch] = useReducer(reducer, initState, init);
- useReducer 接受三个参数:
- reducer:(state, action) => newState
- state: 当前状态值
- action:当前执行操作
- newState:返回一个新的 state
- initState: 初始 state
- init(可选):(initState) => initialState;如果传入了init函数,那么初始的 state 就是 initialState
- reducer:(state, action) => newState
- useReducer 返回一个数组:
- state:action 之后的状态值(即 newState);
- dispatch: (action) => void
- action 参数就是 reducer 的 action 的入参
我们来对比一下 useState:
const [state, setState] = useState(initState)
我们可以看到 setState 可以直接去更改 state,而 useReducer 却多出来了 reducer,然后再返回 state,我们是否可以理解为 reducer 提供了更丰富的功能,可以按需返回 state。
上图我们模拟了 useState 和 useReducer 的运行逻辑图,这么一看下来,useReducer 就 比 useState 极为相似,我们可以看到,useState 中有一个 basicStateReducer (理解为:固定改变 state 的工具) 与 useReducer 中的 reducer(自定义改变 state 的工具),我们可以看一段 updateState 的源码,就能知道它们的联系:
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initailState: any))
}
我们可以看到,当updateState 调用时返回的是 updateReducer,而 basicStateReducer 代替了我们在 useReducer 中自定义的 reducer,因此我们可知:
- useReducer 与 useState 的作用其实都是一样的,就是更新 state。
- useState 处理固定更新的场景,而 useReducer 处理自定义更新场景。
我们先来看一个简单的例子:
import { useState, useReducer } from "react";
const CheckBox = () => {
// useState
// const [checked, setChecked] = useState(false);
// const toggle = () => {
// setChecked(checked => !checked);
// }
// useReducer
const reducer = checked => !checked;
const [checked, toggle] = useReducer(reducer, false);
return (
<>
<input type="checkbox" value={checked} onChange={toggle}></input>
{checked ? '开心' : '不开心'}
</>
)
}
export default CheckBox;
我们可以看到上面注释的部分为 useState 注册的状态,这里我们需要知道 setChecked(checked => !checked) 是 useState 的第二种更新 state 方式(函数式更新),我们再看 useReducer 中的 reducer 方法跟函数式更新一模一样,所以我们可以把 useReducer 理解为 useState 函数式更新的封装,来处理更为复杂的状态管理。
import { useState, useReducer } from "react";
const initUser = {
name: "炭烤小橙微辣",
age: 18,
};
const UserInfo = () => {
const [user1, setUser1] = useState(initUser);
const [user2, setUser2] = useState(initUser);
const [user3, dispatchUser] = useReducer(
(user, newInfo) => ({ ...user, ...newInfo }),
initUser
);
const updateUser1 = () => {
setUser1({ job: "男" });
};
const updateUser2 = () => {
setUser2((user) => ({ ...user, job: "前端" }));
};
const updateUser3 = () => {
dispatchUser({ job: "运动" });
};
return (
<>
<div style={{ paddingLeft: "20px" }}>
<p>
<button onClick={updateUser1} style={{ marginRight: "10px" }}>
添加性别
</button>
user1:
{Object.keys(user1)
.map((key) => user1[key])
.join(" —— ")}
</p>
<p>
<button onClick={updateUser2} style={{ marginRight: "10px" }}>
添加工作
</button>
user2:
{Object.keys(user2)
.map((key) => user2[key])
.join(" —— ")}
</p>
<p>
<button onClick={updateUser3} style={{ marginRight: "10px" }}>
添加爱好
</button>
user3:
{Object.keys(user3)
.map((key) => user3[key])
.join(" —— ")}
</p>
</div>
</>
);
};
export default UserInfo;
同样的数据源,我们点击不同的按钮,用不同的方式对数据进行更新,得到不一样的结果,当我们调用 updateUser1 时,初始对象的属性被清除,证明我们我们用 useState 时不用直接更新,但是我们采用 updateUser2 中更新值时是能达到效果的,反观 useReducer 中的 reducer 方法与 updateUser2 中的方法其实是一样的,只是 useState 需要将结果前置,而 useReducer 是在钩子中得到结果,这样间接的印证了 useState 与 useReducer 效果其实就是一样的。只是我们使用 useReducer 来使得设置值得时候更纯粹一点。当然 useReducer 适用条件如果状态有多个子值或者下一个状态依赖于前一个状态。我在《React 学习手册》 中看到这样一句话给出解释就非常经典了:
授之以鱼不如授之以渔)。
- 鱼: 结果
- 渔:获取结果的方法
tip: 如果文章中有不对的地方,欢迎 diss!