React Ref 其实是这样的( 二 )


如果 ref 回调函数是以内联函数的方式定义的 , 在更新过程中它会被执行两次 , 第一次传入参数 null , 然后第二次会传入参数 DOM 元素 。 这是因为在每次渲染时会创建一个新的函数实例 , 所以 React 清空旧的 ref 并且设置新的 。 通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题 , 但是大多数情况下它是无关紧要的 。
后来的 React.createRefReact.createRef 的优点:

  • 相对于 callback ref 而言 React.createRef 显得更加直观 , 避免了 callback ref 的一些理解问题 。
React.createRef 的缺点:
  1. 性能略低于 callback ref
  2. 能力上仍逊色于 callback ref , 例如上一节提到的组合问题 , createRef 也是无能为力的 。
ref 的值根据节点的类型而有所不同:
  • 当 ref 属性用于 HTML 元素时 , 构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性 。
  • 当 ref 属性用于自定义 class 组件时 , ref 对象接收组件的挂载实例作为其 current 属性 。
  • 默认情况下 , 你不能在函数组件上使用 ref 属性(可以在函数组件内部使用) , 因为它们没有实例:如果要在函数组件中使用 ref , 你可以使用 forwardRef(可与 useImperativeHandle 结合使用)或者可以将该组件转化为 class 组件 。
hooks大家族 useRef这第四种使用 ref 的方法又有何不同呢?
useRef 返回一个可变的 ref 对象 , 其 .current 属性被初始化为传入的参数(initialValue) 。 返回的 ref 对象在组件的整个生命周期内保持不变 。 并且 useRef 可以很方便地保存任何可变值 , 其类似于在 class 中使用实例字段的方式 。
正是由于这些特性 , useRef 和 createRef 出现了很大差异 。
可以运行下以下代码:
import React, { useState, useRef, useEffect } from "react";export default function App() {const [count, setCount] = useState(0);const latestCount = useRef(count);useEffect(() => {latestCount.current = count;});function handleAlertclick() {setTimeout(() => {alert("latestCount.current:" + latestCount.current + '.. count: ' + count);}, 2000);}return (当前count: {count}
setCount(count + 1)}>count + 1提示)}
然后按照下面步骤进行操作:
  1. 连续点击5次 count + 1 按钮
  2. 点击 提示 按钮
  3. 再点击完 提示 按钮后2秒内连续点击2次 count + 1 按钮
  4. 等待 alert 弹窗提示 。
然后会你会得到一个有趣的答案:alert 弹窗会提示: latestCount.current:7.. count: 5 。 使用 useRef 能获取到最新的值 , 但是 useState 却不能 。
具体原因可以参考 react 作者之一 dan 的个人博客 。 或者查看 React 函数式组件和类组件的区别 , 不是只有state和性能!
那么 useRef 真有那么很好用吗?并不是的 。 还有由于它上面的那个特性 , 问题还是不少的 。
你可以尝试跑一下下面这段代码 , 或者 点击这里查看
import React, { useRef, createRef, useState } from "react";import ReactDOM from "react-dom";function App() {const [renderIndex, setRenderIndex] = useState(1);const refFromUseRef = useRef();const refFromCreateRef = createRef();if (!refFromUseRef.current) {// 赋值操作refFromUseRef.current = renderIndex;}if (!refFromCreateRef.current) {// 赋值操作refFromCreateRef.current = renderIndex;}return (Current render index: {renderIndex}在refFromUseRef.current中记住的第一个渲染索引:{refFromUseRef.current}在refFromCreateRef.current中未能成功记住第一个渲染索引:{refFromCreateRef.current}setRenderIndex(prev => prev + 1)}>数值 + 1);}const rootElement = document.getElementById("root");ReactDOM.render(, rootElement);上面的案例中无论如何点击按钮 refFromUseRef.current 将始终为 1 , 而 renderIndex 和 refFromCreateRef.current 会伴随点击事件改变; 意想不到吧?
因为:当 ref 对象内容发生变化时 , useRef 并不会通知你 。 变更 .current 属性不会引发组件重新渲染 。 如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码 , 则需要使用 callback ref 来实现 。
总结下:
  1. useRef 可以获取 DOM ref
  2. useRef 可以获取最新的值
  3. useRef 内容发生改变并不会通知
由于上面的一些问题 , 起初我也是并不想把 useRef 作为操作 ref 的方法来讲的 。
Refs 转发是否需要将 DOM Refs 暴露给父组件?在极少数情况下 , 你可能希望在父组件中引用子节点的 DOM 节点 。 通常不建议这样做 , 因为它会打破组件的封装 , 但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置 。
如何将 ref 暴露给父组件?如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发 。 Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref 。