一文搞定 React 的 ref
创建一个 ref
类组件的 ref
函数
当 ref 是一个函数时,入参就是当前的 DOM 节点,在函数内将它赋给别的值,就可以通过这个值进行访问。
字符串
ref 可以绑定一个字符串,它会被保存到 this.refs 上。
另外当判断 ref 绑定的是字符串时,其实走的是函数的处理逻辑,只不过字符串都被绑定在了 this.refs 上。
createRef()
通过 React 提供的 createRef 来创建一个 ref 对象,ref 对象有一个 current 属性,绑定的值就会保存在这里。
createRef 源码
1 |
|
createRef 的本质就是创建一个带有 current 属性的对象,然后把这个对象返回。
函数组件创建 ref 方式
useRef
和类组件不同的是,函数式组件没有实例,没办法在实例上保存一些东西,所以 React 提供了一系列的 hooks
来帮助函数式组件能够像类组件一样有自己的状态等功能,关于 ref 可以通过使用 useRef 来创建一个 ref 对象。
useRef 源码
初始化 ref
1 |
|
更新 ref
1 |
|
ref 值被保存在 memoizedState 中。
为什么函数式组件不能用 createRef?
看过 createRef 就知道他只是返回了一个带有 current 属性的对象,而函数式组件每次更新时都会重新执行,也就相当于说我们又重新创建了一个 ref,这么做没什么意义。
高阶用法
useRef 缓存数据
在函数式组件内部每次更新状态都会触发视图更新,当有一些数据更新并不需要视图更新的时候,使用 state 难免造成多余的渲染。
这时候使用 useRef
应该是更好的选择,useRef
保存的值更新时不会触发视图更新,
并且 useRef 由于始终指向同一个地址,拿到的永远是最新值,因此 useRef 可以起到一个缓存数据的功能。
ahooks 中的 useLatest
1 |
|
在 ahooks 中的 useLatest 就使用 useRef 来保存值,确保每次都可以拿到最新的值,从而避免闭包问题。
forwardRef 转发 ref
在子组件是类组件时可以通过 ref 直接获取到子组件的实例
但是对于函数式组件来说,不存在实例,也就无法像类组件一样通过 ref 直接通信,针对这个问题 React 提供了 forwardRef 来帮助我们在函数式组件通过 ref 通信。
配合 useImperativeHandle 实现通信
仅仅使用 forwardRef 只能是将 ref 转发出去,但还不足以实现组件间的通信,如果父组件需要调用子组件中的属性/方法,那子组件就需要用 useImperativeHandle 暴漏出去
这里我们将子组件改造一下,使用 useImperativeHandle 暴露出一些属性,再看父组件的输出:
可以看到父组件接收到了子组件暴露出来的属性,这也就完成了通信。
ref 的更新流程
以上代码会发生奇怪的输出情况:
根据输出结果发现每次更新都会有两次输出,一次为 null,一次才是真正的 DOM。在 React 中一次更新分为 render 和 commit 两个阶段,替换真实 DOM 发生在 commit 阶段,而我们的 ref 就是获取真实 DOM 的,所以对于 ref 的处理都在 commit 阶段。至于会变更两次,分别发生在 DOM 更新前和 DOM 更新后,在 DOM 更新前会重置 ref 的值,所以会输出 null,然后 DOM 进行更新,更新后拿到新的 DOM 再次更新 ref,这次输出的是真实 DOM 节点。
流程
- 首先在 commit 的 mutation 阶段(也就是 DOM 更新前),执行 commitDetachRef,将 ref 值置为 null
- 然后 DOM 进行更新
- 在 commit 的 layout 阶段(也就是 DOM 更新后),更新真实节点,执行 commitAttachRef,重新设置 ref 的值
markRef 标记
ref 更新前会进行判断,如果存在 tag 才会进行更新,给 ref 打 tag 的操作就是 markRef
1 |
|
标记的时机就是初始化的时候和ref 指向发生改变,在上文输出中的 null 就是因为 current.ref !== ref 认为发生了更新,打上了 tag ,存在 tag 在执行 commitDetachRef 时把 ref 赋值为 null。
总结
在 react 的日常开发中,ref 的使用是很常见的,本文从创建 ref 开始,分析类组件和函数组件的 ref 区别,以及一些使用 ref 需要注意的地方,在通过例子讲述了 ref 的一些高阶用法,希望能够帮助读者更好的进行开发。