简介
本文主要针对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
一样是异步的,想要立即获取更新后的状态,可以使用useRef
useState
不会同类组件
中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