本文首发于政采云前端团队博客:看完这篇,你也能把 React Hooks 玩出花前端
https://www.zoo.team/article/react-hooks
先讲概念
再总结
Hooks 初识
官方提供的钩子
useRef 、useCallback 、useMemo 、react
useReducer 、useLayoutEffect 、web
useImperativeHandle 、useDebugValuejson
不一样钩子用法
useState
export default function HookDemo() {
const [count, changeCount] = useState( 0);
return (
<div>
{count}
<button onClick={() => { changeCount(Math.ceil(Math.random() * 1000)); }}>
改变count
</button>
</div>
);
}

useEffect
-
函数式组件中不存在传统类组件生命周期的概念,若是咱们须要在一些特定的生命周期或者值变化后作一些操做的话,必须借助 useEffect
的一些特性去实现。 -
useState
产生的 changeState 方法并无提供相似于setState
的第二个参数同样的功能,所以若是须要在 State 改变后执行一些方法,必须经过useEffect
实现。
const [count, changeCount] = useState( 0);
// 将在count变化时打印最新的count数据
useEffect( () => {
message.info( `count发生变更,最新值为${count}`);
}, [count])

useEffect
这个钩子适用状况中的第二种状况,那么如何使用该钩子才能实现相似于类组件中生命周期的功能呢?既然第一个参数是反作用执行的回调,那么实现咱们所要功能的重点就应该在第二个参数上了。
componentDidMount
&& componentWillUnmout
:这两个生命周期只在页面挂载/卸载后执行一次。前面讲过,全部的反作用在组件挂载完成后会执行一次 ,若是反作用存在返回函数,那么返回的函数将在卸载时运行。借助这样的特性,咱们要作的就是让目标反作用在初始化执行一次后不再会被调用,因而只要让与该反作用相关联的状态为空,无论其余状态如何变更,该反作用都不会再次执行,即实现了
componentDidMount
与
componentWillUnmout
。
import React, { useState, useEffect } from 'react';
import { message } from 'antd';
function Child({ visible }) {
useEffect( () => {
message.info( '我只在页面挂载时打印');
return () => {
message.info( '我只在页面卸载时打印');
};
}, []);
return visible ? 'true' : 'false';
}
export default function HookDemo() {
const [visible, changeVisible] = useState( true);
return (
<div>
{
visible && <Child visible={visible} />
}
<button onClick={() => { changeVisible(!visible); }}>
改变visible
</button>
</div>
);
}

componentDidUpdate
:该生命周期在每次页面更新后都会被调用。那么按照以前的逻辑,就应该把全部的状态所有放在第二个状态中,可是这样的话新增/删除一个状态都须要改变第二参数。其实,
若是第二个参数为空,那么在每个 State 变化时都会执行该反作用,那么若是要实现
componentDidUpdate
就很是简单了。
useEffect( () => {
// ...反作用逻辑
}) // 注意上面说的关联状态为空不是说不传递第二个参数,而是第二个参数应该为一个空数组

在类组件中,若是在 componentDidMount
中屡次调用setState
设置一个值( 固然不推荐这样作),并在成功的回调中打印该值,那么最后的结果极可能会打印不少个相同的最后一次设置的值。是由于类的setState
是一个类异步的结果,他们会将全部变更的内容进行收集而后在合适的时间去统一赋值。而在 useEffect
中,全部的变量的值都会保留在该反作用执行的时刻,相似于 for 循环中的 let 或者 闭包,全部的变量都维持在反作用执行时的状态,也有人称这个为 Capture Value。
useCallback
useEffect
中存在的相同逻辑的封装,减小代码冗余,配合
useEffect
使用。
const [count1, changeCount1] = useState( 0);
const [count2, changeCount2] = useState( 10);
const calculateCount = useCallback( () => {
if (count1 && count2) {
return count1 * count2;
}
return count1 + count2;
}, [count1, count2])
useEffect( () => {
const result = calculateCount(count, count2);
message.info( `执行反作用,最新值为${result}`);
}, [calculateCount])

useCallback
的使用生成了一个回调,
useCallback
的使用方法和
useEffect
一致,第一个参数为生成的回调方法,第二个参数为该方法关联的状态,
任一状态发生变更都会从新生成新的回调。
useEffect
不一样的地方在于使用
useCallback
生成计算的回调后,在使用该回调的反作用中,
第二个参数应该是生成的回调。其实这个问题是很好理解的,咱们使用
useCallback
生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会从新生成新的回调,反作用监听到了回调的变化就会去从新执行反作用,
此时 useCallback
和 useEffect
是按顺序执行的, 这样就实现了反作用逻辑的抽离。
useRef
useRef
接受一个参数,为 ref 的初始值。相似于类组件中的
createRef
方法 ,该钩子会返回一个对象,对象中的 current 字段为咱们
指向的实例 /
保存的变量,能够实现得到目标节点实例或保存状态的功能。
useRef
保存的变量不会随着每次数据的变化从新生成,而是保持在咱们最后一次赋值时的状态,依靠这种特性,再配合 useCabllback
和 useEffect
咱们能够实现 preProps/preState
的功能。
const [count, changeCount] = useState( 0);
const [count1, changeCount1] = useState( 0);
// 建立初始值为空对象的prestate
const preState = useRef({});
// 依赖preState进行判断时能够先判断,最后保存最新的state数据
useEffect( () => {
const { ... } = preState.current;
if ( // 条件判断) {
// 逻辑
}
// 保存最新的state
preState.current = {
count,
count1,
}
});

useState
建立的状态赋值给
useRef
用做初始化时,
手动更改 Ref 的值并不会引发关联状态的变更。从该现象来看,
useRef 彷佛只是在内存空间中开辟了一个堆空间将初始化的值存储起来,该值与初始化的值存储在不一样的内存空间,修改 Ref 的值不会引发视图的变化。
export default function HookDemo() {
const [count] = useState({ count: 1 });
const countRef = useRef(count);
return (
<div>
{count.count}
<button onClick={() => { countRef.current.count = 10; }}>
改变ref
</button>
</div>
);
}

useMemo
useMemo
即便用记忆的内容。该钩子主要用于作性能的优化。
useEffect
会所有执行。一样的,
经过计算出来的值或者引入的组件也会从新计算/挂载一遍,即便与其关联的状态没有发生任何变化。
shouldComponetUpdate
以及
React.memo
useMemo
这个钩子。
useMemo
来处理计算结果的缓存或引入组件的防止重复挂载优化。其接受两个参数,第一个参数为一个 Getter 方法,
返回值为要缓存的数据或组件,第二个参数为该返回值相关联的状态,当其中任何一个状态发生变化时就会从新调用 Getter 方法生成新的返回值。
import React, { useState, useMemo } from 'react';
import { message } from 'antd';
export default function HookDemo() {
const [count1, changeCount1] = useState( 0);
const [count2, changeCount2] = useState( 10);
const calculateCount = useMemo( () => {
message.info( '从新生成计算结果');
return count1 * 10;
}, [count1]);
return (
<div>
{calculateCount}
<button onClick={() => { changeCount1(count1 + 1); }}>改变count1</button>
<button onClick={() => { changeCount2(count2 + 1); }}>改变count2</button>
</div>
);
}

useMemo
时可能咱们会以为该钩子只是用来作计算结果的缓存,返回值只能是一个数字或字符串。其实
useMemo
并不关心咱们的返回值类型是什么,它只是在关联状态发生变更时从新调用咱们传递的 Getter 方法 生成新的返回值,也就是说
useMemo
生成的是 Getter 方法与依赖数组的关联关系。所以,若是咱们将函数的返回值替换为一个组件,那么就能够
实现对组件挂载/从新挂载的性能优化。
import React, { useState, useMemo } from 'react';
import { message } from 'antd';
function Child({ count }) {
return <p>当前传递的count为:{count}</p>;
}
export default function HookDemo() {
const [count1, changeCount1] = useState( 0);
const [count2, changeCount2] = useState( 10);
const child = useMemo( () => {
message.info( '从新生成Child组件');
return <Child count={count1} />;
}, [count1]);
return (
<div>
{child}
<button onClick={() => { changeCount1(count1 + 1); }}>改变count1</button>
<button onClick={() => { changeCount2(count2 + 1); }}>改变count2</button>
</div>
);
}

其余钩子
useLayoutEffect
useImperativeHandle
useDebugValue
,
useContext
,是 createContext 功能在函数式组件中的实现。经过该功能能够实现不少强大的功能,能够是说官方的 Redux,不少人对此应该有很多的了解。该钩子内容太多,后续单独使用一个章节进行描述。
编写本身的钩子
最基本的钩子
import React, { useState } from 'react';
// 编写咱们本身的hook,名字以use开头
function useCounter(initialValue) {
// 接受初始化的值生成state
const [count, changeCount] = useState(initialValue);
// 声明减小的方法
const decrease = () => {
changeCount(count - 1);
}
// 声明增长的方法
const increase = () => {
changeCount(count + 1);
}
// 声明重置计数器方法
const resetCounter = () => {
changeCount( 0);
}
// 将count数字与方法返回回去
return [count, { decrease, increase, resetCounter }]
}
export default function myHooksView() {
// 在函数组件中使用咱们本身编写的hook生成一个计数器,并拿到全部操做方法的对象
const [count, controlCount] = useCounter( 10);
return (
<div>
当前数量:{count}
<button onClick={controlCount.decrease}>减小</button>
<button onClick={controlCount.increase}>增长</button>
<button onClick={controlCount.resetCounter}>重置</button>
</div>
)
}
useCounter
这个钩子中建立了一个关联了
initialValue
的状态,并建立减小/增长/重置的方法,最终将其经过
return
返回出去。这样在其余组件须要用到该功能的地方,经过调用该方法拿到其返回值,便可实现对
useCounter
组件封装逻辑的复用。

返回 DOM 的钩子
import React, { useState } from 'react';
import { Modal } from 'antd';
function useModal() {
const [visible, changeVisible] = useState( false);
const toggleModalVisible = () => {
changeVisible(!visible);
};
return [(
<Modal
visible={visible}
onOk={toggleModalVisible}
onCancel={toggleModalVisible}
>
弹窗内容
</Modal>
), toggleModalVisible];
}
export default function HookDemo() {
const [modal, toggleModal] = useModal();
return (
<div>
{modal}
<button onClick={toggleModal}>打开弹窗</button>
</div>
);
}

钩子/最终总结
钩子总结

useState
与
useRef
外,其余钩子都存在第二个参数,第一个方法的执行与第二个参数相互关联。因而咱们能够得出一个结论,在使用了 Hook 的函数式组件中,咱们在使用
反作用/引用子组件时都须要时刻注意对代码进行性能上的优化
。
最终总结
Hooks 组件的目标并非取代 class component 组件,而是增长函数式组件的使用率,明确通用工具函数与业务工具函数的边界, 鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数。
招贤纳士
ZooTeam@cai-inc.com
本文分享自微信公众号 - 前端迷(love_frontend)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。数组