🕸 依赖收集(Dependencies Collection)

@shined/react-use 作为基础的底层工具库,性能是重中之重,为了在确保灵活性、功能丰富的前提下尽可能地减少性能开销,我们引入了依赖收集(Dependencies Collection)的概念。

依赖收集的想法最初来源于 SWR,在此基础上我们进行了一些改进和优化。

简而言之

  • 为了提供灵活、丰富的功能,许多 Hooks 内部需要管理和返回许多状态(State)
  • 用户可能并不需要消费所有的状态,只用到了其中的一部分,但是任意状态变化都会导致重渲染
  • 为了减少不必要的渲染开销,我们引入了「依赖收集(Dependencies Collection)」的概念
  • 只有用到的状态发生变化时才会触发渲染,这对用户来说是无感的,但是可以显著提升性能

动机

有许多 Hooks 内部管理和返回了许多状态,用户可能只需要其中的一个或某几个状态,但是任意状态变化都会导致重渲染,造成不必要的性能开销。

例如,在 @shined/react-use 内部有一个 useQuery,它为了实现诸多功能特性,返回了包括但不限于 dataloadingerror 等多个状态。正常情况下,无论是否使用了返回的状态,只要对应状态发生了变更,内部就会调用类似 setState(loading) 的操作来更新状态。

这个操作逻辑本身并没有问题,问题在于,当用户实际上并未使用某些状态时仍然会进行更新,导致了不必要的渲染开销,极大降低了渲染性能。

// 用户实际上只需要 run 操作和 loading 这一个状态,对于 error、data 其实并不关心 const { run, loading } = useQuery(requestSomething, options) // 用户执行 run 操作 run() // 当 data、error、loading 改变时,内部会触发 `setState` 操作导致组件重新渲染

在某些场景下(如上),我们可能只需要其中的一个状态,比如 loading,而不需要其他状态。为了实现这一点,我们引入了依赖收集的概念,这对用户是无感的,但是可以显著提升性能。

// 用户需要什么,就从返回值里面取什么 const { run, data } = useQuery(requestSomething, options) // 用户执行 run 操作 run() // 当 data 改变时,内部会触发 `setState` 操作导致组件重新渲染 // 而对于 error、loading,即使改变了也不会触发重新渲染,因为用户并没有实际用到

也就是说,对用户来说,需要什么状态就取什么状态,无关的状态不会触发重新渲染。这样一来,原本需要渲染 45 次的组件,现在可能只需要渲染 12 次,这对于性能提升来说,无疑是相当巨大的。

实现了依赖收集的 Hooks

由于工作量巨大,我们目前只对部分 Hooks 进行了依赖收集的优化,未来会逐步对其他 Hooks 进行优化。

实现原理

实现原理其实很简单,只需要在内部维护一个 ref,同时在暴露返回值时,通过 getter 函数进行标记,当用户使用到某个状态时,将这个状态标记为「已使用」,在变更时判断是否需要触发重新渲染。

下面是一个简单的伪代码来演示依赖收集的底层原理:

// 使用 useRef 而不是 useState 来定义状态,因为我们希望手动控制状态的更新 const stateRef = useRef({ name: { used: false, value: 'react-use' }, age: { used: false, value: 18 }, }) // 通过 setState 函数来手动更新状态,同时判断是否需要触发重新渲染 function setState(key: string, value: any) { if(stateRef.current[key].value === value) return stateRef.current[key].value = value if (stateRef.current[key].used) render() } // 返回值通过 getter 函数暴露,同时在 getter 函数中标记状态是否被使用 const returnedState = { get name() { stateRef.current.name.used = true return stateRef.current.name.value }, get age() { stateRef.current.age.used = true return stateRef.current.age.value }, }

通过这样的方式,我们可以实现一个简单的依赖收集系统,只有当用户使用到了某个状态时才会触发重新渲染。在 @shine/react-use 内部,这一整套逻辑被整合到 useTrackedRefState() Hook (目前尚未对外暴露)当中,用于实现依赖收集的功能。