简介
本文主要针对React `16.8.x`提供的`hooks`使用加以介绍,更高版本的中的`hooks`暂无介绍
优势
- 代码量少(最直观的体现)
- 相较于
类组件使用HOC、render props,hooks更为简单方便的复用状态组件——状态处理逻辑复用(后面详细介绍
- 100%向后兼容,与
类组件可同时使用 - 不需要考虑
this相关的问题 - 相较于
类,函数更容易被机器理解 - 相较于通过生命周期分割代码,不相干的逻辑放在同一个生命周期函数中,通过功能区分,放在不同的函数中,代码更容易理解和维护
劣势
- 使用不当性能问题可能会比
类组件要严重 - 不能使用
decorator(装饰器)
官方API
useState
这个hooks应该是用的最多的了,
useState 可接受一个参数为,任意类型数值 或者 可以返回任意类型数值的函数,
const [count, setCount] = useState(initialCount);
const [count, setCount] = useState(() => {
//do something
return resultCount
})
然后返回一个数组,数组第一个值为当前组件最新的 state ,只能通过数组的第二个值来更新,第二个值为更新 state 的工具函数(该工具函数可以接受一个 值 作为参数,为更新后 state 的结果;也可以接受一个 函数 作为参数,函数的参数为 state 的前一个值,如果返回值与当前 state 相同则不会重新渲染组件)
function Demo({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
{count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
tip
类组件可以直接对this.state.something赋值(虽然不会更新视图),但在hooks 组件中不能直接对state赋值hooks 组件中state的更新函数同类组件中setState一样是异步的,想要立即获取更新后的状态,可以使用useRefuseState不会同类组件中setState一样是合并更新,而是直接覆盖更新useState是基于useReducer的,建议:简单的数据结构,不同的状态放在不同的useState中,复杂的数据结构直接使用useReducer。useState参数为函数时,函数只会在初始化是执行一次
const formate = (data) => {
// do some complicated things
return result;
}
const [state, setState] = useState(formate(props.data)); // bad; formate每次渲染都要执行
const [state, setState] = useState(() => formate(props.data)); // good
useReducer
const [state, dispatch] = useReducer(reducer, initialState, initialAction);
useReducer 使用方式类似 redux ,适用于复杂状态的存储,同步更新状态,以及深层次更新组件状态。支持三个参数,第一个参数为 reducer 函数,第二个参数为初始状态initialState , 第三参数为一个函数(可选),用于对 initialState 初始化处理
const reducerFun = (state, action) => {
switch (action.type) {
case 'add':
return {count: state.count + 1}
case 'reduce':
return {count: state.count + 1}
default:
return state
}
}
const initialState = {count: 0};
const initialAction = (init) => {
return {
count: init.count + 1
}
}
function Demo() {
const [state, dispatch] = useReducer(reducerFun, initialState, initialAction)
return (
<div>
{state.count}
<button onClick={() => dispatch({type: 'add'})}>
+
</button>
<button onClick={() => dispatch({type: 'reduce'})}>
-
</button>
</div>
)
}
tip
- 深层次更新组件状态,可以将
dispatch作为props传给子组件用于状态更新 - 使用
useState获取的setState方法更新数据时是异步的;而使用useReducer获取的dispatch方法更新数据是同步的。
useEffect
useEffect(didUpdate, deps);
useEffect 有支持两个参数,第一个参数为 effect (副作用)函数,每次render之后执行,,这个函数可以有返回值,倘若有返回值,返回值也必须是一个函数,姑且称它为 清除函数 ,会在组件被销毁时执行(这句话是片面的)。其实不单是在组件销毁时执行,当组件更新时,清除函数 的函数的执行时机会被放置到,新一次组件 render 之后执行,然后再执行 effect 函数中非 清除函数部分。
如以下demo:
function Demo() {
const [count, setCount] = useState(0)
const [random, setRandom] = useState(0)
return (
<div>
{count % 2 == 0 && <Child count={count} random={random}/>}
<button onClick={() => setCount(s => s+1)}> + </button>
<button onClick={() => setRandom(Math.random())}>random</button>
</div>
)
}
function Child(props) {
useEffect(() => {
console.log('mounted')
return () => {console.log('unmount')}
})
console.log('render')
return (<h1>{props.count} {props.random}</h1>)
}

可以看出当 Child 组件销毁时,执行了 清除函数 ,Child 组件创建时,先 render 然后执行了 effect 函数。但是更新 Child 组件时先 render 然后执行了 清除函数 ,然后才是 effect 函数
第二个参数可选,为一个数组,组件重新渲染之后,当数组中有值发生了改变时便会执行副作用函数,否则不执行。
function Demo() {
const [count, setCount] = useState(0)
const [count1, setCount1] = useState(0)
useEffect(() => {
console.log('count') // 只有 count发生改变时才会打印,count1改变不会
}, [count])
return (
<div>
{count}
<button onClick={() => setCount((s) => s + 1)}>
count+
</button>
<button onClick={() => setCount1((s) => s + 1)}>
count1+
</button>
</div>
)
}
如果想要只在组件初始挂载或者卸载时执行副作用只需将第二个参数置为 []
function Demo() {
useEffect(() => {
console.log('只有初始挂载或者卸载时执行')
}, [])
return (
<div> </div>
)
}
tip
effect函数的执行时机是在组件创建或更新render之后,如果想要在render时同步触发副作用可以使用useLayoutEffect- 不同于
class组件在各生命周期中的各种副作用的处理,建议将不同的副作用放置不同的useEffect中通过功能进行区分,便于阅读理解
useLayoutEffect
useLayoutEffect 的使用形式和 useEffect 是相同的,它们之间的唯一区别是: useLayoutEffect 会在所有 DOM 变更之后同步调用。于是,可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新。与 componentDidMount、componentDidUpdate 的调用时机是一样的。因为是同步调用,可能会产生视觉更新阻塞的问题,所以尽可能使用标准的 useEffect 以避免阻塞视觉更新
tip
- 执行
DOM更新操作时useLayoutEffect会比useEffect更适合使用 - 涉及使用逐帧动画
requestAnimationFrame时,注意执行时机:useLayoutEffect > requestAnimationFrame > useEffect
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(), deps);
useMemo 和 useEffect 两者语法同样是一致的,两者区别是 useEffect 执行的是副作用,一定是在渲染后执行的,useMemo 是需要返回值的,返回值直接参与渲染,因此 useMemo 是在渲染时执行的
先举个反例:
function Demo(props) {
const [count, setCount] = useState(1);
const calculate = (count) => {
let num = null;
// num = 炒鸡复杂的处理逻辑
return num;
}
return (
<div count={calculate(count)}>
</div>
)
}
Demo组件每次渲染的时候都会执行calculate这个炒鸡复杂的计算过程,而参数count偏偏没有改变,每次重复的计算,是不是很浪费?这时候就可以使用useMemo来优化了。
如下代码,
function Demo(props) {
const [count, setCount] = useState(1);
const number = useMemo(() => {
let num = null;
// num = 超级复杂的处理逻辑
return num;
}, [count])
return (
<div count={number}>
</div>
)
}
只有在count发生了改变才会重新来执行这个炒鸡复杂的计算,没有改变时就直接拿过来之前的计算结果来用,是不是很方便?
tip
- 注意
deps参数的设置,避免更新错误 - 传入
useMemo的函数会在渲染期间执行。请不要在函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect的适用范畴,而不是useMemo useMemo的另一常规用法就是和Rect.memo搭配使用减少子组件重复渲染,后文会有详细介绍
useCallback
const memoizedCallback = useCallback(() => {/*do something*/}, deps);
useCallback 用法和用途与 useMemo 类似,同样支持两个参数,第一参数为要缓存的函数,第二个为判断是否更新的依赖数组,其主要区别在于 useCallback 返回值为函数只能用于缓存函数,useMemo 可以用于缓存值和函数。可用于缓存事件回调函数。
同样先举个反例:
function Demo(props) {
const clickHandle = () => {
let num = null;
// num = 超级复杂的处理逻辑
return num;
}
return (
<div onClick={clickHandle}>
</div>
)
}
每次组件更新的时候 clickHandle 都需要重新定义,显然是没有必要的,因此可以引入useCallback:
function Demo(props) {
const clickHandle = useCallback(() => {
let num = null;
// num = 超级复杂的处理逻辑
return num;
}, [])
return (
<div onClick={clickHandle}>
</div>
)
}
tip
- 我们可以认为:
useCallback(fn, deps)等同于useMemo(() => fn, deps) useCallback的另一常规用法就是和Rect.memo搭配使用减少子组件重复渲染,后文会有详细介绍
useRef
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。
function Demo(props) {
const box = useRef(null);
return (
<div onClick={clickHandle} ref={box}>
</div>
)
}
不同于类组件中的ref,它不仅仅适用了DOM元素和类组件的引用,在hooks 组件中可以使用其保存任何值,且在组件的整个生命周期内保持不变。可以直接修改.current 属性的值且不触发组件更新。
function Demo(props) {
const num = useRef(0);
const [evenNum, setEvenNum] = useState(0)
const add = () => {
num.current += 1;
if(!(num.current & 1)) {
setEvenNum(num.current)
}
}
return (
<h1 onClick={add}>
{evenNum}
</h1>
)
}

如图,当单次数点击时数字不会更新,偶次数点击时数字更新
useImperativeHandle
useImperativeHandle(ref, () => ({}))
在类组件中想要在父组件中想要获取子组件实例,只需通过ref属性直接获取,然后就可以调用子组件属性或方法,而useImperativeHandle就提供了在hooks 组件中实现该功能的方法;
function Demo() {
const child = useRef(null);
return (
<div>
<Child ref={child}/>
<button onClick={() => {child.current.add()}}>add</button>
</div>
)
}
function Child(props, ref) {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
add: () => setCount((s) => s + 1)
}))
return (
<h1>
{count}
</h1>
)
}
Child = React.forwardRef(Child)
useContext
const value = useContext(MyContext);
useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值,能够在hooks 组件中读取 context 的值以及订阅 context 的变化。
const MyContext = React.createContext('test')
function Demo() {
const [count, setCount] = useState(0)
return (
<div>
<MyContext.Provider value={count}>
<Child/>
</MyContext.Provider>
<button onClick={() => setCount(s => s + 1)}>+</button>
</div>
)
}
function Child(props) {
const myContextValue = useContext(MyContext)
return (
<h1>
{myContextValue}
</h1>
)
}
tip
- 当组件上层最近的
<MyContext.Provider>更新时,该Hook会触发重渲染,并使用最新传递给MyContext provider的context value值。即使祖先使用React.memo或shouldComponentUpdate,也会在组件本身使用useContext时重新渲染。 useContext(MyContext)相当于static contextType = MyContext在类中,或者<MyContext.Consumer>。- 搭配
useReducer可以实现简易版redux
memo
memo 不属于 hooks, 却是为 hooks 组件 量身定做的一个 API ,是一个 HOC ,只能用于 函数组件 不能用于 类组件
function MyComponent(props) {}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
类似 类组件 的 React.PureComponent 和 shouldComponentUpdate(),对传入组件的 props 进行浅对比或者自定义对比,来决定组件是否需要重新渲染,已达到性能优化的目的。
首先看一个demo:
const MyContext = React.createContext('test')
function Demo() {
const [count, setCount] = useState(0);
const [contextNum, setContextNum] = useState(0);
const [childNum, setChildNum] = useState(0);
const addChildNum = () => {
setChildNum(s => s + 1)
}
const formateChildNum = (n) => ({value: n + 's'});
return (
<div>
<h1>
fatherNum: {count}
</h1>
<MyContext.Provider value={contextNum}>
<Child childNum={formateChildNum(childNum)} addChildNum={addChildNum}/>
</MyContext.Provider>
<button onClick={() => setCount(s => s + 1)}>add fatherNum</button>
<button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
</div>
)
}
function Child({childNum, addChildNum}) {
const myContextNum = useContext(MyContext)
console.log('render Child')
return (
<div>
<h1>
myContextNum: {myContextNum}
</h1>
<h1>
childNum: {childNum.value}
</h1>
<button onClick={addChildNum}>add childNum</button>
</div>
)
}

可以看出当父组件中fatherNum改变组件更新时Child组件也随之重新渲染了,尽管渲染前后并无变化,这显然是一次无意义的渲染。这只是一个简单的demo,父组件只有一个state改变,子组件只是多了一次渲染,或许无关痛痒,但是如果父组件state、props比较多改变比较频繁,而子组件又十分的‘复杂’,却额外多余渲染 N 次,就在用户体验上就很可能带来很差的影响了。
首先我们引入memo,再次只改变fatherNum:
+ Child = memo(Child)
function Child({childNum, addChildNum}) {
const myContextNum = useContext(MyContext)
console.log('render Child')
return (
<div>
<h1>
myContextNum: {myContextNum}
</h1>
<h1>
childNum: {childNum.value}
</h1>
<button onClick={addChildNum}>add childNum</button>
</div>
)
}

可以看到,引入 memo 后改变 fatherNum,父组件重新渲染以后,子组件仍然触发了无意义的重渲染。和预想的并不一样。原因在于:虽然传给 Child 组件 props 的值没有什么改变,但是,由于每次hooks 组件重新渲染以后,组件函数会重新执行一遍,因此 props 中childNum 变成了一个新的object 对象,同样 addChildNum 函数也被重新定义了;(内存空间变了)。这时就需要用到 useMemo 和 useCallback了。
优化后
function Demo() {
const [fatherNum, setFatherNum] = useState(0);
const [contextNum, setContextNum] = useState(0);
const [childNum, setChildNum] = useState(0);
// const addChildNum = () => {
// setChildNum(s => s + 1)
// }
const addChildNum = useCallback(() => {
setChildNum(s => s + 1)
},[]);
// const formateChildNum = (n) => ({value: n + 's'});
const resultChildNum = useMemo(() => ({value: childNum + 's'}), [childNum]);
return (
<div>
<h1>
fatherNum: {fatherNum}
</h1>
<MyContext.Provider value={contextNum}>
<Child childNum={resultChildNum} addChildNum={addChildNum}/>
</MyContext.Provider>
<button onClick={() => setFatherNum(s => s + 1)}>add fatherNum</button>
<button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
</div>
)
}

自定义hooks
自定义hooks本质也是函数,不同于一般函数的是,其内部可以使用useState等API做状态管理。正是因为有了它存在,才有了hooks 组件 相对于 类组件 最大的优势 ———— 状态处理逻辑复用。自定义hooks的存在允许我们将 UI组件 与 无UI组件 相分离。
栗子
https://github.com/streamich/react-use 一个不错的自定义hooks库,除了可以拿来用以外还可以参考源码,总结自己的经验也是很不错的。
tip
自定义hooks共享复用的是状态处理逻辑,是逻辑,而不是单纯的状态本身,每hooks都是独立的
注意事项
- 不要从常规
JavaScript函数调用hooks; - 不要在循环,条件或嵌套函数中调用
hooks; - 必须在组件的顶层调用
hooks; - 可以从
React功能组件调用hooks; - 可以从自定义
hooks中调用hooks; - 自定义
hooks必须使用use开头,这是一种约定; - 建议将不需要
props或state的函数提到组件外部。组件每次都会重新渲染,函数每次重新定义,因此一些不必要的函数可以提到函数外面;
最后
本文是对 作者根据自己的经验以及网上收集到的资料,对 hooks 使用以及注意事项的介绍,可以满足日常开发使用的需要。如果想要详细了解 其内部的实现机制,与其去看他人消化后的产物,不如直接阅读源码:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js
看完感觉有一点点收获的话,还望不吝点赞~
转载:https://blog.csdn.net/zSY_snake/article/details/106085046