我们组的前端妹子在组内分享时谈到了 react 的钩子, 趁此机会我也对我所理解的内容进行下总结, 方便更多的同学了解. 在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API, 我们非常有必要了解下他的使用方法, 并能够结合我们的业务编写几个自定义的 hooks.
1. 常用的一个 hooks
官方中提供了几个内置的钩子, 我们简单了解下他们的用法.
1.1 useState: 状态钩子
需要更新页面状态的数据, 我们可以把他放到 useState 的钩子里. 例如点击按钮一下, 数据加 1 的操作:
- const [count, setCount] = useState(0);
- return (<>
- <p>{ count}</p>
- <button onClick = {
- () => setCount(count + 1)
- }> add 1 </button>
- </>
- );
在 typescript 的体系中, count 的类型, 默认就是当前初始值的类型, 例如上面例子中的变量就是 number 类型. 如果我们想自定义这个变量的类型, 可以在 useState 后面进行定义:
const [count, setCount] = useState<number | null>(null); // 变量 count 为 number 类型或者 null 类型
同时, 使用 useState 改变状态时, 是整个把 state 替换掉的, 因此, 若状态变量是个 object 类型的数据, 我只想修改其中的某个字段, 在之前 class 组件内调用 setState 时, 他内部会自动合并数据.
- class Home extends React.Component {
- state = {
- name: 'wenzi',
- age: 20,
- score: 89
- };
- update() {
- this.setState({
- score: 98
- }); // 内部自动合并
- }
- }
但在 function 组件内使用 useState 时, 需要自己先合并数据, 然后再调用方法, 否则会造成字段的丢失.
- const [person, setPerson] = useState({
- name: 'wenzi',
- age: 20,
- score: 89
- });
- setPerson({
- ...person,
- {
- score: 98
- }
- }); // 先合并数据 { name: 'wenzi', age: 20, score: 98 }
- setPerson({
- score: 98
- }); // 仅传入要修改的字段, 后 name 和 age 字段丢失
1.2 useEffect: 副作用钩子
useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合.
useEffect 钩子在组件初始化完毕时, 一定会执行一次, 在组件重新渲染的过程中, 是否还要 update, 还要看传入的第 2 个参数.
当只有回调函数这一个参数时, 组件的每次更新, 回调都会执行;
当有 2 个参数时, 只有第 2 参数里的数据发生变化时, 回调才执行;
只想在组件初始化完毕时只执行一次, 第 2 个参数可以传入一个空的数组;
我们可以看下这个例子, 无论点击 add 按钮 还是 settime 按钮 ,useEffect 的回调都会执行:
- const Home = () => {
- const [count, setCount] = useState(0);
- const [nowtime, setNowtime] = useState(0);
- useEffect(() => {
- console.log('count', count);
- console.log('nowtime', nowtime);
- });
- return ( <>
- <p>count: {count} </p>
- <p>nowtime: {nowtime} </p>
- <button onClick = {() => setCount(count + 1)}> add 1 </button>
- <button onClick = {() => setNowtime(Date.now())}> set now time </button>
- </>);
- };
若改成下面的这样, 回调仅会在 count 发生变化时才会在控制台输出, 仅修改 nowtime 的值时没有输出:
- useEffect(() => {
- console.log('count', count);
- console.log('nowtime', nowtime);
- }, [count]);
useEffect 的回调函数还可以返回一个函数, 这个函数在 effect 生命周期结束之前调用. 为防止内存泄漏, 清除函数会在组件卸载前执行. 另外, 如果组件多次渲染, 则在执行下一个 effect 之前, 上一个 effect 就已被清除.
基于上面的代码, 我们稍微修改一下:
- useEffect(() => {
- console.log('count', count);
- console.log('nowtime', nowtime);
- return () => console.log('effect callback will be cleared');
- }, [count]);
基于这个机制, 在一些存在添加绑定和取消绑定的案例上特别合适, 例如监听页面的窗口大小变化, 设置定时器, 与后端的 websocket 接口建立连接和断开连接等, 都可以预计 useEffect 进行二次的封装, 形成自定义的 hook. 关于自定义 hook, 下面我们会讲到.
1.3 useMemo 和 useCallback
function 组件中定义的变量和方法, 在组件重新渲染时, 都会重新重新进行计算, 例如下面的这个例子:
- const Home = () => {
- const [count, setCount] = useState(0);
- const [nowtime, setNowtime] = useState(0);
- const getSum = () => {
- const sum = ((1 + count) * count) / 2;
- return sum + ',' + Math.random(); // 这个 random 是为了看到区别
- };
- return ( <>
- <p> count: {count}</p>
- <p> sum: {getSum()}</p>
- <p> nowtime: {nowtime}</p>
- <button onClick = {() => setCount(count + 1)}> add 1 </button>
- <button onClick = {() => setNowtime(Date.now())}> set now time </button>
- </>);
- };
这里有 2 个按钮, 一个是 count+1, 一个设置当前的时间戳, getSun() 方法是计算从 1 到 count 的和, 我们每次点击 add 按钮后, sum 方法都会重新计算和. 可是当我们点击 settime 按钮时, getSum 方法也会重新计算, 这是没有必要的.
这里我们可以使用 useMemo 来修改下:
- const sum = useMemo(() => ((1 + count) * count) / 2 + ',' + Math.random(), [count]);
- <p> {
- sum
- } </p>;
修改后就可以看到, sum 的值只有在 count 发生变化的时候才重新计算, 当点击 settime 按钮的时候, sum 并没有重新计算. 这要得益于 useMemo 钩子的特性:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 返回回调里 return 的值, 而且 memoizedValue 它仅会在某个依赖项改变时才重新计算. 这种优化有助于避免在每次渲染时都进行高开销的计算. 如果没有提供依赖项数组, useMemo 在每次渲染时都会计算新的值.
在上面的例子里, 只有 count 变量发生变化时, 才重新计算 sum, 否则 sum 的值保持不变.
useCallback 与 useMemo 类型, 只不过 useCallback 返回的是一个函数, 例如:
- const fn = useCallback(() => {
- return ((1 + count) * count) / 2 + ',' + nowtime;
- }, [count]);
2. 实现几个自定义的 hook
在官方文档里, 实现了好友的在线与离线功能. 这里我们自己也学着实现几个 hook.
2.1 获取窗口变化的宽高
我们通过监听 resize 事件来获取实时获取 Windows 窗口的宽高, 对这个方法进行封装后可以在生命周期结束前能自动解绑 resize 事件:
- const useWinResize = () => {
- const [size, setSize] = useState({
- width: document.documentElement.clientWidth,
- height: document.documentElement.clientHeight
- });
- const resize = useCallback(() => {
- setSize({
- width: document.documentElement.clientWidth,
- height: document.documentElement.clientHeight
- })
- }, [])
- useEffect(() => {
- Windows.addEventListener('resize', resize);
- return () => Windows.removeEventListener('resize', resize);
- }, []);
- return size;
- }
使用起来也非常方便:
- const Home = () => {
- const {width, height} = useWinResize();
- return <div>
- <p>width: {width}</p>
- <p>height: {height}</p>
- </div>;
- };
点击链接 useWinResize 的使用可以查看 demo 演示.
2.2 定时器 useInterval
在前端中使用定时器时, 通常要在组件生命周期结束前清除定时器, 如果定时器的周期发生变化了, 还要先清除定时器再重新按照新的周期来启动. 这种最常用的场景就是九宫格抽奖, 用户点击开始抽奖后, 先缓慢启动, 然后逐渐变快, 接口返回中奖结果后, 再开始减速, 最后停止.
我们很容易想到用 useEffect 来实现这样的一个 hook:
- const useInterval = (callback, delay) => {
- useEffect(() => {
- if (delay !== null) {
- let id = setInterval(callback, delay);
- return () => clearInterval(id);
- }
- }, [delay]);
- };
我们把这段代码用到项目中试试:
- const Home = () => {
- const [count, setCount] = useState(0);
- useInterval(() => {
- console.log(count);
- setCount(count + 1);
- }, 500);
- return <div> {
- count
- } </div>;
- };
可是这段运行后很奇怪, 页面从 0 到 1 后, 就再也不变了, console.log(count) 的输出表明代码并没有卡死, 那么问题出在哪儿了?
React 组件中的 props 和 state 是可以改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果, 它们之间不再有相关性.
useEffect() Hook 也「丢弃」上一次渲染结果, 它会清除上一次 effect 再建立下一个 effect, 下一个 effect 锁住新的 props 和 state, 这也是我们第一次尝试简单示例可以正确工作的原因.
但 setInterval 不会「丢弃」. 它会一直引用老的 props 和 state 直到你把它换掉 -- 不重置时间你是无法做到的.
这里就要用到 useRef 这个 hook 了, 我们把 callback 存储到 ref 中, 当 callback 更新时去更新 ref.current 的值:
- const useInterval = (callback, delay) => {
- const saveCallback = useRef();
- useEffect(() => {
- // 每次渲染后, 保存新的回调到我们的 ref 里
- saveCallback.current = callback;
- });
- useEffect(() => {
- function tick() {
- saveCallback.current();
- }
- if (delay !== null) {
- let id = setInterval(tick, delay);
- return () => clearInterval(id);
- }
- }, [delay]);
- };
当我们使用新的 useInterval 时, 发现就可以自增了, 点击查看样例 useInterval 的简单使用.
这里我们使用一个变量来控制增加的速度:
- const [count, setCount] = useState(0);
- const [diff, setDiff] = useState(500);
- useInterval(() => {
- setCount(count + 1);
- }, diff);
- return ( <div>
- <p> count: {count} </p>
- <p> diff: {diff}ms </p>
- <p>
- <button onClick = {() => setDiff(diff - 50)}> 加快 50ms </button>
- <button onClick = {() => setDiff(diff + 50)}> 减慢 50ms </button>
- </p>
- </div>);
分别点击两个按钮, 可以调整 count 增加的速度.
3. 总结
使用 react hook 可以做很多有意思的事情, 这里我们也仅仅是举几个简单的例子, 后续我们也会更加深入了解 hook 的原理.
▼我是来腾讯的小小前端开发工程师,
长按识别二维码关注, 与大家共同学习, 讨论▼
来源: https://www.cnblogs.com/xumengxuan/p/11882489.html