React16.8 useHook 特性
提示
React16.8 迎来了大众期盼已久的函数式编程利器:useHook
特性。它能大大增强函数式组件的能力,使得立即执行的、没有生命周期与 state 等等类组件特性的函数式组件拥有相同的能力。
useHook
的原理实际上是利用了 JavaScript 的闭包机制,因此在多次执行函数的同时记住一些状态。
useHook
拥有多种函数,我们也可以自定义自己的 hook。在这里主要来说说其中最常用的几种,简述他们的大致原理,以及使用时的注意事项。
useState
useState
就是为了给纯函数组件加入 class 组件中的 state 能力。它返回一个有两个元素的数组,第一个元素是需要设置的 state 变量,第二个是改变这个变量的 setter 函数。useState
的入参决定了返回变量的初始值。
const [flag, setFlag] = useState(false)
在每一次执行 setter 之后,使用了useState
的整个函数组件都会被重新执行一次。但是此时useState
函数本身并不会被再次执行。这是因为实际上,useState
利用了闭包的特性,在闭包内设置了一个私有变量。事实上 setter 改变的值是这个私有变量,我们能取出的变量是这个私有变量的 getter 返回值。
注意,只能在函数的最外层调用 Hook,不能在循环、条件判断或子函数中使用。
这是因为,为了支持在同一个函数组件中使用多次useState
,在闭包中,被useState
的赋值的私有变量本质上是一个数组类型,通过函数首次被调用的useState
的顺序来决定被赋值变量的索引位置,最后 getter 通过索引顺序找到希望取得的变量值。若在循环、条件判断或子函数中使用,则有可能造成函数组件重新执行时顺序与首次执行不一致,这将导致useState
的取值混乱。
useEffect
useEffect 为纯函数组件提供了 class 组件中的componentDidMount
、componentDidUpdate
、componentWillUnMount
、shouldComponentUpdate
这些生命周期能力。
useEffect 接收两个参数,第一个参数是一个函数,在函数中执行的动作相当于在类组件中生命周期中的所需要进行的工作。该函数的返回值也是一个函数,若不为空的话它执行的时机等同于componentWillUnMount
。
第二个参数是一个数组,传入的是需要监听变量列表,若填写该值,只有当监听范围内的变量更新时,才执行第一个参数函数中的动作。起到了类似shouldComponentUpdate
的作用。当传入空数组时相当于不监听任何变量,只在组件生成时执行一遍,等同于componentDidMount
。当传入为空值时相当于监听所有变量,等同于componentDidUpdate
。
function fetchSth() {
/* 执行异步操作 */
}
function destroySth() {
/* 执行 unmount 操作 */
}
useEffect(() => {
fetchSth()
return detroySth()
},[])
useReducer
useReducer
事实上与 redux 概念中的reducer
十分类似,如果使用过 redux 应该对reducer
的概念和定义不会陌生。
它与useState
作用场景相似,也能赋予函数组件设定和处理 state 的能力。通常情况下,它们是可以互换的。
但是,对于在组件中存在着多个 state 的定义,或者是操作逻辑复杂需要同时更改多个 state,又或者定义的 state 是一个复杂类型,如数组、对象或存在嵌套,使用useReducer
会使得 state 声明更加集中,同时操作逻辑更为清晰,代码也能有更好的可读性。
// 第一个参数:应用的初始化
const initialState = {count: 0, isDone: false};
// 第二个参数:state 的 reducer 处理函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {...initialState, count: state.count + 1, isDone: true};
case 'decrement':
return {...initialState, count: state.count - 1, isDone: true};
default:
return initialState
}
}
const [state, dispatch] = useReducer(reducer, initialState);
// 用法
function onIncrementClick() {
dispatch({type: 'increment'})
}
useMemo 和 useCallback
这两个 API 我拿在一起来说,原因很简单,他们的目标都是一致的,都是缓存结果,只不过useCallback
是缓存函数本身,useMemo
是缓存函数返回的结果。从这里也可以看出,实际上,useCallback
是可以被useMemo
通过多包裹一层函数实现的。
这两个 API 的函数签名与 useEffect 基本一致,因此调用方法区别不大。在用法功能上,他们之间区别最大的一点是,useEffect 是处理副作用的,是在 render 函数执行完后执行的,相当于 class 组件的didMount/didUpdate
。而这两个 API 不能处理副作用,必须同步调用,即在哪里调用就在哪里立即执行函数内容,在下一行就能直接使用返回的结果。
那么,这两个 API 的使用情景是什么呢?答案是性能优化。我们先来看看需要使用useMemo
的情况:
function keywordChangeCompute(keyword) {
/* 执行一些非常消耗资源的同步操作 */
}
const [keyword, setKeyword] = useState('')
useMemo(() => {
keywordChangeCompute(keyword)
},[keyword])
若不使用useMemo
而直接将函数调用写在 render 中的话,当外部组件或者该组件的其他 state 进行频繁的更新时,该组件会不断地多次触发keywordChangeCompute
函数,造成不必要的性能损失。而对于使用了useMemo
来说,只在keyword
变化的时候才会触发一次函数调用,这与useEffect
的触发机制是一致的。
对于useCallback
的使用,有一种情况是非常实用的,即该方法会被传给子组件的情况:
const [status, setStatus] = useState(false)
const onChange = (value) => {
fetch(`/api/get/${value}/${status}`)
}
return (
<>
<ChildComponent onChange={useCallback(onChange,[status])}>
</>
)
因为父组件在每一次更新的时候都会生成一个全新的onChange
函数,而由于status
状态不变,新的onChange
函数并没有改变,这样会导致子组件进行一次没必要的更新,造成性能损失。而使用了useCallback
后,只会在status
状态更新时生成新的函数传给子组件,从而减少子组件渲染次数。