react hook
react hook
技术学习|2023-10-15|最后更新: 2024-2-5
type
status
date
slug
summary
tags
category
icon
password
Blocking
Blocked by
top
URL
Sub-item
Parent item

动机

  1. 在组件之间复用状态逻辑很难
  1. 复杂组件变得难以理解
  1. 难以理解的 class
  1. 函数组件没有生命周期,没有状态

原理

hooks 的实现就是基于 fiber 的,会在 fiber 节点上新增一个memorizedState 属性,按顺序存放hook上一次渲染的缓存数据,然后不同的 hooks api 使用对应的数据来完成不同的功能。
memorizedState是一个链表,会在第一次调用时mount,后面只需要update。
对于effect副作用钩子,会绑定在workInProgress.updateQueue上,等到commit阶段,dom树构建完成,在执行每个 effect 副作用钩子。

react 运行

mount阶段(初始化)

  1. 生成当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
  1. 初始化 Hook 的状态,即读取初始 state 值
  1. 创建一个新的链表作为更新队列,用来存放更新操作(setXxx())
  1. 创建一个 dispatch 方法(即 useState 返回的数组的第二个参数:setXxx()),该方法的用途是用来修改 state,并将此更新操作添加到更新队列中,另外还会将该更新和当前正在渲染的 fiber 绑定起来
  1. 返回当前 state 和 修改 state 的方法(dispatch)

update阶段(更新)

  • 获取正在执行的处于更新阶段 Hook 节点;
  • 获取该 Hook 节点的更新队列链表;
  • 从该更新队列的最早的 update 对象节点开始遍历,一直遍历到最近添加的(最新的)update 对象节点,遍历到每个节点的时候执行该节点的更新操作,将该次更新的 state 值存到 newState 中;
  • 当遍历完最近的一个 update 对象节点后,此时 newState 里存放的就是最新的 state 值,最后返回 newState,于是用户就拿到了最新的 state;

优缺点

优势:
  • 简洁:react hooks解决了hoc和render props的嵌套问题,更加简洁 (在不使用class的情况下,使用state及react其他特性(省的把纯函数组件/其他组件改来改去))
  • 解耦:react hooks可以更方便地把UI和状态分离,做到更彻底的解耦
  • 组合:hooks 中可以引用另外的hooks 形成新的hooks, 组合千变万化
  • 函数友好:react hooks为函数组件而生,解决了类组件的几大问题处理了this的指向问题让组件更好的复用(老的class组件冗长、包含自身的状态state)让react编程风格更取向函数式编程风格,使用function代替class
缺点(坑):
  • 【useState数组修改】使用useState修改array的值时,不要使用push/pop/splice等直接更改数据对象的方法,否则无法修改,应该使用解构或其他变量代替。
  • 【hook执行位置】不要在循环、条件 、嵌套中调有hook,必须始终在react函数顶层使用Hook,这是因为react需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数,否则会导致调用顺序不一致性,从而产生难以预料到的后果。
  • 响应式的useEffect: 当逻辑较复杂时,可能触发多次。
  • 闭包陷阱(useCallback):函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包场景可能引用到旧的state、props值),希望输出最新内容的话,可以使用useRef来保存state。
  • 破坏了pureComponent、react.memo 浅比较的性能优化效果(为了取最新的props和state,每次render都要重新创建事件处函数)
  • react.memo 并不能完全替代 shouldComponentUpdate (因为拿不到 state change ,只针对 props change)

注意点

hook不能写在判断语句里

react hook底层是基于链表(Array)实现,React 需要依赖 Hook 调用的顺序来确定每个 Hook 对应的缓存状态,如果 Hook 调用的顺序发生改变,可能会导致程序出现错误或不符合预期的行为。
而在 if 和 for 中,由于它们的执行次数是动态的,可能会导致 Hook 的调用顺序发生改变,从而引发问题。例如,在某个循环中使用 useState Hook,由于循环次数是不定的,Hook 的调用顺序也就无法确定,会导致状态更新混乱。

useEffect和useLayoutEffect区别?

useEffect相比componentDidMount/componentDidUpdate不同之处在于,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这让应用响应更快,大多数据情况下,effect不需要同步地执行,个别情况下(例如测量布局),有单独的useLayoutEffect hook可使用,其API与useEffect相同
useEffect在副使用结束之后,会延迟一段时间执行,并非同步执行。遇到dom操作,最好使用userLayoutEffect
userLayoutEffect 里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制前运行完成,阻塞了浏览器的绘制

举例

 
打印结果: 0 0 0 0 0
执行一次setNumber的流程:
1. 执行setNumber后,会导致组件function重新,所有语句会被重新调用执行
2. 走到useState的时候,react内部其实走了updateState,拿到最新的状态。
3. 走到handerClick,此时handerClick被重新创建,即handerClick指向了新的内存空间,值得注意的是:for循环接着往下走的时候,num所在的上下文并不是当前函数(新创建的函数),而是第一次初始化时创建的函数,而那个函数的上下文中,num永远都是0,所以console.log输出都是0
相反:class写法,state发生变化handerClick没有被重新创建而已,并且this指向也没发生改变
 

基础hook使用注意点

useState

初始值

如果初始值不是一个函数,则每次都会重新计算,如下:
解决方法: 使用函数式更新

嵌套数据

因为react的immutable特性,setState必须返回一个新数据,但如果在修改一个嵌套数据时就很麻烦。
解决方法:
  1. 深克隆(不推荐)
深克隆对象,然后进行修改。但问题是深克隆对象的性能很差,而且深拷贝后的新对象,react会认为对象里的数据都是全新的,所有有使用对象的组件都会重新渲染,即使前后值没有变化。
  1. 使用immer.js可以直接修改数据。有两种方式:

useEffect

返回值

useEffect 的返回值是一个函数,这个函数用于清除订阅的事件。当组件被卸载或者重新渲染时,React 会自动执行这个函数,以清除事件的订阅。
  1. 如果依赖数组为空,那么 useEffect 只会在组件挂载时执行一次副作用操作,再卸载时执行return里的函数
  1. 如果依赖数组存在,那么useEffect也会在state修改时执行。

useCallback

性能优化

使用 useCallback 的主要场景是在优化性能时,避免不必要的重新渲染(re-render)。
常见的误区,是为了避免每次渲染都重新创建函数,但是在大多数情况下,javascript 创建一个函数的开销是很小的,哪怕每次渲染都重新创建,也不会有太大的性能损耗。
真正的性能损耗在于,每次渲染时都会创建一个新的函数对象,这可能会导致子组件不必要的重新渲染。
使用useCallback 可以用于缓存函数的引用,避免在每次渲染时创建新的函数对象,维持函数的引用。(也可以使用 React.memo 包括子组件来避免)

useContext

重复渲染

当 Context 中的某个属性发生变化时,所有使用该 Context 的子组件都会被重新渲染,即使某些组件并未使用到这个属性。
解决方法
  1. 使用其他的状态管理库,例如Zustand。
  1. 使用react-tracked三方库
react-tracked基于 proxy 和组件内部的 useForceUpdate 做到了自动化的追踪,可以精准更新每个组件,不会出现修改大的 state,所有组件都刷新的情况。
 
 

扩展

httpswebpack、vite
Loading...