Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

    • 开始上手
    • useState 和 useReducer
    • useEffect
    • useRef 原理
      • 目录
      • useRef 定义
      • mountRef: useRef on mount phrase
      • updateRef: useRef on update phrase
      • Q&A
      • 参考文档
  • 总结

  • React源码漂流记

  • react
  • hooks原理
jonsam
2022-04-14
目录

useRef 原理

# 目录

  • 目录
  • useRef 定义
  • mountRef: useRef on mount phrase
  • updateRef: useRef on update phrase
  • Q&A
  • 参考文档

# useRef 定义

首先我们来看一下 useRef 在 React 中的定义代码 (react package):

export function useRef<T>(initialValue: T): {current: T} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
1
2
3
4

可以看出如下信息:

  • 接受初始值 initialValue,返回值携带在 current 上。
  • useRef 接受 dispatcher 的调度,在不同的环境可能有不同的实现。

下面来看一下 useRef 在 dispatcher 上是如何实现的:

在 HooksDispatcherOnMount 中 useRef 的实现为 mountRef,在 HooksDispatcherOnUpdate 中 useRef 实现为 updateRef。看来大多 hook 的实现模式与此类似。

# mountRef: useRef on mount phrase

mountRef 的实现很简单,基本上只是做初始化的工作。

function mountRef<T>(initialValue: T): {current: T} {
  // 获取当前正在执行的 hook
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  // 初始化 current 值到 hook
  hook.memoizedState = ref;
  return ref;
}
1
2
3
4
5
6
7
8
9
10
11

# updateRef: useRef on update phrase

在更新阶段,只需将缓存的值取出即可,缓存的值存在 memoizedState 中。

function updateRef<T>(initialValue: T): {current: T} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
1
2
3
4

# Q&A

标签: 重要

看到这里,可能有以下几个问题:

  1. 既然 useRef 只是在 render 过程中去缓存值,那么完全可以将之以变量的方式定义在组件前面,那个他存在的意义是什么?两者又有什么区别?

首先,我们需要知道的是,useRef 其实是解决了 useState 闭包陷阱的问题。useState 一定能够更新值,但是有一种特例会使代码得不到 useState 更新后的值,那就是闭包环境,这种特例叫做闭包陷阱。

这种现象的产生主要与 useState 更新 primitive value 有关,而更新 object 则不存在这种问题。主要原因是 object 是存在堆中的,变量的保存的只是 object 的引用,而 primitive value 则不同。

闭包陷阱是如何产生的呢?请看下面的代码。

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
}
1
2
3
4
5
6
7
8

在这种情况下,无论 setCount 怎么执行,打印出的 count 值都是 1。我们来分析下程序执行的过程:

首先,在 mount 阶段程序执行到 useState 会将 count 的初始值设置为 1,然后执行到 useEffect,则设置定时器。由于 useEffect 的依赖数组为 [],只会在 mount 时执行一次。然后通过应用的某些操作触发 setCount,count 的改变,因此在非闭包的环境下,count 的值更新无误,然而在定时器中由于形成的闭包环境,会记录 count 的值为 1,没有感知的 count 值的变化。如果此处将 count 写成对象的方式,在 setCount 时使用 Object.merge 不改变对象的引用,则即使在对象中也能感知到 count 的变化。

那么闭包环境是如何形成的呢?

Closures (opens new window) from MDN:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

核心理解

useRef 是如何巧妙避免闭包陷阱的呢?原理正在 object 在变量中只保存引用,因此 useRef 正是在 React 内部维持了 {current: value} 的对象,我们在使用 current 中的值或者是给 current 赋值时,都不会导致包裹 current 的外层对象的引用变化,这就保证了外层的包括对象永远只存在内存的统一地方,而 current 作为引用永远会指向我们赋给 current 的任何值。

回到主题,避免闭包陷阱有两种方式,一种是使用 useRef,另外一种就是使用组件外的变量。useRef 能避免闭包陷阱的原因上述已经解释清楚了,那么组件外的变量又为什么能解决这种问题呢?我们知道 FC 本质是函数,React 正在是靠执行 FC 来完成 render 过程的(从 renderWithHook 函数中 children = Component(props, refOrContext); 可以看出这一点)。我们可以把 React 的 render 过程看成是视频播放的帧。既然 React 是执行 FC 达到 render 的目的,而组件外层的变量则不会在 render 的过程中被反复执行,因此这些变量只执行一次,确实是可以达到缓存变量的目的。

核心理解

但是需要注意的是,useRef 的不可替代性正是体现在下面的两点:

  1. useRef 的缓存作用,且不会引起 re-render;
  2. useRef 是与组件实例挂钩的,不同组件实例中 useRef 互不干扰。

我们知道,组件存在的最大目的就可复用性。组件从面向对象的层面思考就是一个对象,而组件的引用可以理解为一个对象实例。相同对象中不同实例之间的变量和函数互不影响。实际上,这正是由闭包机制形成的。

  1. 为什么要把缓存的结构设置为 {current: value} 的结构?这和 vue3 中 ref () 的结构类似,两者有什么异同?

从上一问题中,我们已经知道 useRef 解决闭包陷阱的关键就在于其对象结构,因此这里使用 primitive value 这种结构是绝对不可以的。因此写 current 也是十分必要的。

至于 vue3 中 ref 需要使用 .value 也是类似的原因。虽然 Proxy API 支持 object 属性的 get、set 的监听,但是 ref 其实并不是 Proxy 实现的,通过源码可知,ref 是以 class 实现的 ref 对象,并且自定义拦截了 get 和 set 方法。因此 ref 一方面要保证 ref 对象的地址不会变化,以供我们随时的引用,同时提供了对 ref.value 属性做 get、set 的监听。 value 属性的 get 操作会被 track,其 set 操作则会被 trigger。相比于 React 中 useRef,vue3 中的 ref 通过实现对 .value 属性的 track 和 trigger 以实现其响应式。

# 参考文档

  • 从 react hooks “闭包陷阱” 切入,浅谈 react hooks (opens new window)
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
useEffect
开始上手

← useEffect 开始上手→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式