jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React - useEffect 依赖项填坑 (二) #21

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

React - useEffect 依赖项填坑 (二)

参考文章:

前言

没想到关于 useEffect 依赖项的问题居然会有第二版。不过确实,关于 useEffect 依赖项中引入引用类型的问题,至今仍有一点疑惑,几经思考有所感悟,故按照自己的想法记录一下。 回到正题,这次碰到的疑惑是: 项目中需要将一个业务逻辑分离出来,因此需要自定义一个 hook,这个 hook 内部包含了一个 useEffect,而自定义 hook 的参数接收了一个 useState 返回的 state 对象,伪代码如下:

// main.js
const [state, setState] = useState({a: 1, b: 2});
useHook( state );

// 自定义 hook
const useHook = ( state ) => {
  useEffect(()=>{
    // do something...
  }, [ state ])
}

问题来了:请问 main.js 中组件每次发生重新渲染 ( 非 setState 触发 ) 时,自定义 hook 内的 useEffect 会被排入队列吗? 接下来我们将从以下几点对这个问题做一个解答:

  1. 函数传参是值传递
  2. useState 的状态存储机制
  3. useEffect 的依赖项浅比较

函数参数值传递

js 中函数参数始终遵循值传递的原则,即拷贝外部变量作为函数内部作用域的参数。其中原始类型的拷贝是一个一模一样的副本,内外拷贝互不干涉影响,引用类型则拷贝和传递对应的指针,在函数内部修改引用类型参数,会影响到外部的变量。

换句话说,引用类型的传参,函数内外的引用指针(地址)指向是一样的。

useEffect 依赖项浅比较

useEffect 对于依赖项的比较是通过 Object.is() 实现的,在比较引用类型时,只要引用类型的指针地址相同,那么就不会执行 useEffect 内部的函数。换句话说,只要一个引用类型它不重复声明或创建,那它就是稳定的。

在 useEffect 依赖项第一弹中提到对象间始终不相等 {} !== {} ,与这里矛盾?并不是,对象间始终不相等是因为比较的是两个变量,这个问题等价于:

let a = {};
let b= {};
console.log( a === b ); // false

useState 的状态存储

在 useState 中,state 是被保存在当前组件所维护对象的状态单元中的,因此,对于 state 的比较而言,函数传参时传递的引用类型指针,始终都指向同一个地方,也就是该 state 在状态单元中所存储的位置。所以,只要 state 没有被 setState 修改,它的指针地址和存储是不会发生改变的,也就不会触发 useEffect。

React在每个组件的幕后维护一个对象,并且在这个持久对象中,有一个“状态单元”数组。当你调用useState时,React将该状态存储在下一个可用的单元格中,并递增数组索引。 假设你的 hooks 总是以相同的顺序调用(如果遵循 hooks 的规则,它们将是相同的顺序),React能够查找特定useState调用的前一个值。对useState的第一个调用存储在第一个数组元素中,第二个调用存储在第二个元素中,依此类推。

关于 react 组件所维护的对象,请参考:

流程

我们来捋一下整个流程:

  1. 组件开始渲染:执行函数组件
  2. 首先在函数组件中会调用 useState 创建 state 和 setState,并将 state 存入记忆单元格。

    组件本身维护了一个「记忆单元格」对象,该对象在首次渲染时初始化,并在 hook 调用时读取对应的内容,然后移动指针到下一个 hook 调用。

  3. 然后调用自定义 hook,将 state 传入 hook 函数,因为是首次传递,因此会触发一次 useEffect,useEffect 为记录本次的依赖项 (---- state 的指针地址) 作为下一次 Object.is() 比较的依据。
  4. ... 组件渲染结束
  5. 由于某种原因 (除 setState 外),函数组件被重新渲染了,重复上述 1 ~ 4 的步骤,但不同的是,useState 仅在组件初次渲染时被调用,因此实际上没有第 2 步,直接进入第 3 步。由于没有使用 setState,因此该组件所维护的记忆单元格中 state 对象的指针地址和实际存储并没有发生改变,因此再一次调用 useEffect 时,传入的 state 还是相同的指针地址副本,此时 useEffect 依赖项是没有发生变化的。
  6. 假设我们调用了 setState 更新 state,setState 会更新返回一个新的 state 对象 (保证 immutable ),并将组件重渲染推入队列中等待执行,此时记忆单元格中存储当前 state 的位置就会替换上新的 state 对象以及对应的指针地址,那么该次改动就会被自定义 hook 中的 useEffect 侦察到,并执行内部的副作用函数。