# why hook?
在我学 react 时,恰好刚学完 vue3 中的 compositionAPI,对 hook 有一个初步的了解,hook 不同于 vue2 中的选项式定义数据方法或是 react 类组件中对状态的集中管理,它更像是 code 调用了一个库,而非框架调用 code,使得复用性更强了。
那么为什么不用 mixin 呢? mixin 模式下伴随着隐式依赖,代码冲突覆盖等问题,使得 vue 和 react 都已经放弃使用 mixin 来复用逻辑
类组件作为一种面向对象思想的提醒,但由于其功能的堆加在使得代码臃肿,后期维护和 tree shaking 难度过高,hook 便孕育而生
# hooks
# 响应式数据
# useState
1 | const [state, setState] = useState(initData) |
state
就是响应式数据,与 vue 不同的是,hook 需要用 setState
去重新为 state
赋值才能响应式刷新页面, initData
则是数据初始值
# useContext
useContext 类型于 vue 中的 provide Inject,需要在父组件中设置 Provider,然后再子组件中使用
1 | const themes = { |
# useReducer
1 | const [state, dispatch] = useReducer(reducer) |
这个 hook 有点类似于 redux 的状态管理方案,但我个人不太喜欢,推荐使用 recoil
接下来介绍三个 react18 新加入的数据钩子
# useSyncExternalStore
1 | useSyncExternalStore(subscribe,getSnapshot,getServerSnapshot) |
- subscribe 为订阅函数,当数据改变时触发 subscribe,通过比较 getSnapshot 决定是否更新数据
- getSnapshot 是对数据的缓存,当变化时响应更新页面
- getServerSnapshot 用于 hydration 模式下替代 getSnapshot
这个 hook 类似于订阅 store,store 变化触发回调,目前运用的场景可能是持久化存储 store
# useTransition
1 | const [isPending , startTransition] = useTransition() |
- isPending 为处理状态标识,若处理中则为 true,处理完毕则为 false
- startTransition 接受一个回调函数,该回调函数中所有工作 (同步或异步) 处理完毕之后才会将 isPending 置 false
useTransition
主要运用于 loading 场景,当一些数据处于网络请求加载中时,我们可以将其添加一个暂时的 loading 界面提高用户的体验,而不是白屏等待
业务上如通过切换 tab 时,tab 为非网络响应任务,则可以立即完成,而获取数据渲染列表则为网络响应任务,需要等待,我们可以用该 hook 优化
# useDeferredValue
1 | useDeferredValue() |
该 hook 与上面类似,将任务推迟执行,推迟到 react 所有工作执行完成后再执行,但不同的是, useTransition
侧重于过程, useDeferredValue
侧重于状态,
可以用该 hook 将某个诸如输入框的状态进行防抖
# 数据派生缓存
组件更新时导致重新渲染时,所有的非缓存代码都会重新执行,若某些状态或函数传递给子代 props,则会间接导致子组件也进行了重复的渲染,造成性能的浪费,为了避免于此,我们需要使用 memorize (记忆)
# useMemo
1 | const memo = useMemo(() => state+1, [state]) |
用过 vue 的话,对于 computed 和 pinia 的 getter 都不会感到陌生,这个 hook 跟其相似,都是对状态的派生缓存,只有当第二个参数数组中的 state 改变时,才会重新进行计算缓存,算是性能优化的一种方式
常常用于缓存大量数据的计算结果,防止组件其他无关状态变化而刷新组件导致重复计算丢失性能
# useCallback
1 | const memoFn = useCallback( |
千万不要拿此 hook 与 vue 的 watch 相提并论,这完全不是同一个东西,如同 useMemo 缓存状态(数据),此 hook 缓存的是函数,当组件更新时,函数会重复执行,造成性能浪费,该 hook 就是解决了这一个问题,若是函数还要传入给子组件,那么可以使用该钩子优雅地解决性能问题(子组件需要 memo)
# memo
memo 这个并不是 hook,而是一个 HOC(高阶函数)包裹需要缓存的组件(与缓存相关所以提一下),多与上述两个 hook 搭配使用,被缓存的组件只有在 prop 变化时才会重新渲染
1 | export default memo(function app() { |
# 副作用
副作用钩子的出现主要解决了函数组件没有生命周期的缺陷,但对其的使用必须有熟悉的生命周期概念,才能更好地理解,下面对组件生命周期进行一个新的概述。
# useEffect
1 | useEffect(() => { |
刚开始对该钩子开始的理解,以为他是生命周期钩子的替代品,但如果是这么想就大错特错
- useEffect 是异步的(生命周期钩子是同步的会阻塞线程)
- useEffect 执行的时间点再 render 后,也就是 4 后(该时间点没有对应的钩子)
下面举个例子来讨论 useEffect 用法
1 | // APP |
通过实验我们可以得出结论(分 5 段,若数组为空,则只有 1、4、5)
1、在一开始组件 render 时会执行一次副作用函数
2、当我们点击 + 1 更新数据时,会先执行 return 中的函数(阶段 5),也就是在下一个副作用函数之前且数据还是旧的(并非 beforeUpdated,该钩子执行时虚拟 dom 已经生成完毕了,数据是最新的),然后 setState 更新之后再次执行副作用函数打印出最新数据,对应新页面生命周期 render
3、再次加一同上
4、当我们点击卸载组件后,执行一次 return 中的回调函数 (阶段 5)
5、此时我们再次点击卸载组件将组件挂载,得到与 1 相同的结果
一般可以在 useEffect 中进行数据请求,设置和清除定时器,操作 dom 绑定事件等操作
# useLayoutEffect
1 | useLayoutEffect(() => { |
该 hook 采用同步执行,算是真正意义上的生命周期钩子
当第二个参数数组中为空时,hook 可作为 mounted 使用,真实 dom 生成但页面还没渲染时执行一次第一个参数中的回调函数,在此钩子中执行 dom 操作会比在 useEffect 中好,主要是为了避免浏览器再次回流重绘造成的闪屏
当第二个参数数组中填入状态(数据)时,hook 作为类似 vue 中的 watch 使用,状态更新时调用第一个参数中的回调函数 (页面首次生成真实 dom 时也会执行一次回调函数)
为什么这个不是 updated 而是 watch 呢?updated 和 watch 两者根本上都是状态 (数据) 改变导致页面重新渲染而触发函数,虽然触发动机略有差别,但最终结果都是一致的。显然该 hook 更像 watch,是数据改变驱动函数执行,但其执行的时间点恰好是在 updated,所以也可以当作 updated 来用
第一个参数中的回调函数 return 时与 useEffect 作用相同
该 hook 中的回调函数会阻塞浏览器的绘制
因为如此,react 官方更推荐使用 useEffect 完成网络请求更新数据等操作
# useInsertionEffect
1 | export default function Index(){ |
该 hook 执行时间点比 useLayoutEffect 更早(阶段 2),也就是最早执行的副作用钩子,执行时真实 dom 还未更新,主要的运用场景时解决 CSS in JS 渲染中注入样式的性能问题,其他场景估计用不太到
# 访问 dom
# useRef
众所周知,react 是数据驱动型的 MVVM 框架,但某些场景我们仍需要直接操作 dom 来实现业务需求
1 | export default function hook() { |
另外 useRef 还可以用来保存状态
1 | export default function hook() { |
# useImperativeHandle
父组件可以将状态和函数通过 prop 传给子组件,子组件想将数据和函数暴露给父组件也是有方法的,在 vue 中我们可以通过 defineExpose 来决定暴露的状态方法并在父组件中获取组件 ref 的方式调用数据方法,在 react 中我们同样可以用 useImperativeHandle 搭配 forwardRef 来实现 defineExpose 相同的功能
1 | //子组件 |
我们先将子组件用高阶函数 forwardRef 暴露出去,暴露之后 forwardRef 会传给子组件一个 ref 作为第二个参数 (第二个参数 ref 只在使用 React.forwardRef 定义组件时存在),这个 ref 指向子组件本身,我们将该 ref 作为 useImperativeHandle 的第一个参数,并且在第二个参数中写入我们需要暴露的属性方法
然后在父组件中我们使用 ref 的方式访问组件 dom 来获取上面的属性和方法
# 工具 hook
# useDebugValue
该 hook 主要是为了在 react 开发工具中显示自定义 hook 的标签,检查自定义 hook
# useId
此 hook 是 react18 的新 hook,它可以在客户端和服务端生成唯一的 id,解决服务端渲染中,两端产生 id 不一致的问题,更重要是保障了 react18 中的 streaming renderer(流式渲染)中 id 的稳定性
# 总结
以上就是所有 hook 的介绍,hook 的出现大大方便了 react 的使用,我一开始学 react 时是从类组件开始的,类组件的使用让人非常难受,this 问题一直让我怀疑 react 真的那么好用嘛?后来接触到 hook 才知道 react 真正牛逼的地方,这也是 vue3 中 compositionAPI 的前身吧。
官方强烈推荐使用函数式组件和 hook 的搭配使用来彻底代替类组件的使用,那么我们也没有理由再去学习类组件了,哪怕是还没接触过 react 的小白,所以现在就彻底拥抱 react 函数组件和 hook 吧!!!😄
不得不说,vue3 和 react 真的越来越像同一个模子的框架了,template+compositionAPI 和 jsx+hook 的使用竟十分相像,以至于我从 vue3 转 react 的时候十分的平滑(react 文档尽快重置啊!!!里面很多类组件的用法看的我头大)