学习
实践
活动
专区
工具
TVP
写文章
专栏首页我用reactReact Ref 为什么是对象
原创

React Ref 为什么是对象

你是否想过 React 中 ref 的用法是 ref.current 而不是直接通过 ref 获得我们想要的数据,这个包含 current 属性的对象结构是多此一举吗?毕竟它有且仅有 current 这一个属性。const ref = useRef(null); // 声明 ref console.log(ref.current); // 使用 ref 为什么不直接设计成 console.log(ref)先说结论,React Ref 的数据结构设计成 JavaScript Obeject 是为了让数据在其他作用域中也能被正确地读取

在React 函数式组件(FC)中,我们使用 useRef hook 来声明 ref 数据,可能你对 ref 特性或者 useRef hook 并不熟悉,这里有一篇文章深入浅出地介绍了 useRef 及其应用场景

总结一下这篇文章的知识点就是:

  • ref 数据和 state 数据不同点在于,ref 更新时组件不会更新(重走一遍函数作用域)
  • 由于 ref 的上述特性,它常常可以用作保存无需响应式更新UI的数据,用的最多的是保存某个 DOM 节点对象的引用

一个截图的例子

笔者负责了一个开发业绩长图的需求,场景是将一篇比较丰富的海报用 DOM 还原出来供用户预览,再通过类似于“截图”的方式将海报下载成图片。业内截图用的比较多的是 html2canvas

附上简单代码。

const App = () => {
  const reviewRef = useRef(null) // 创建 ref,用于引用 DOM 节点对象

  /**
   * 点击下载按钮后进行简单的保存 DOM 为图片并下载的逻辑
   */
  const onClick = () => {
    reviewRef.current &&
      html2canvas(reviewRef.current, {}).then((canvas) => {
        downloadByB64({
          fileName: "report.jpg",

          b64: `${canvas.toDataURL()}`,
        })
      })
  }

  return (
    <div>
      <button onClick={onClick}>下载图片</button>
      {/* 以下是预览区域 */}
      <article ref={reviewRef}>
        <h1>标题</h1>
        <p>内容</p>
      </article>
    </div>
  )
}

简单梳理代码过程如下

  • App 组件内声明了 ref 数据 reviewRef,声明了回调函数 onClick,App 函数作用域返回 jsx 代码,将 onClick 回调函数设置为 button 元素的 click event handler,当页面中的App组件渲染完毕后,reviewRef 和 article 元素形成一对一的关系,具体表现为 review.ref 为 article 的 DOM 元素引用
  • 当用户点击下载图片 button,onClick 回调函数执行,完成预期的下载操作。

UI和逻辑分离

领导建议组件中UI代码和逻辑代码分离,这样对团队成员的协同开发和代码的可读性都有好处。UI代码即 jsx 代码,逻辑代码包括 hook 代码和各种回调函数代码,将逻辑代码抽成自定义 hook 代码,第一反应是从上述代码抽解出自定义的下载图片 hook(后称 useDownload hook ),useDownload hook 唯一依赖于 DOM 节点,因此我很自然地将 DOM 节点即 reviewRef.current 当做函数参数传入 useDownload hook

/**
* 下载预览区域为图片的业务逻辑钩子
*/
const useDownload = (el) => {
  const onClick = () => {
    el &&
      html2canvas(el).then((canvas) => {
        downloadByB64({
          fileName: "report.jpg",
          b64: `${canvas.toDataURL()}`,
        })
      })
  }

  return onClick
}

const App = () => {
  const reviewRef = useRef(null)
  const onClick = useDownload(reviewRef.current)

  return (
    <div>
      <button onClick={onClick}>下载图片</button>
      {/* 以下是预览区域 */}
      <article ref={reviewRef}>
        <h1>标题</h1>
        <p>内容</p>
      </article>
    </div>
  )
}

但是这样写出来代码却并不符合预期,一番 debug 过后,发现在点击下载图片按钮,执行 onClick 回调的过程中,el 的值为一直为 null ,而并非 DOM 元素对象的引用,因此也就无法将元素下载成图片。

原因是什么呢?🧐❓

按照 React 运作的时序来分析,当函数组件 App 的最后一段 return 代码执行完后, ref.current 值从 null 被更新为 DOM 元素对象的引用,代码执行完毕,函数作用域被回收。因此,在 useDownload hook 被调用的时刻,被传递的参数 ref.current 很明显是 null,而在 ref.current 被更新的时候,hook 的传参却没有更新,即数据过期了。

想当然的解决办法就是在 ref.current 数据更新后,重新调起一次 useDownload 函数作用域,hook 代码被重新执行一遍,以确保拿到的形参数据是最新的。这种重新渲染组件的要求可以通过更新组件状态的方式间接实现,代码简单示例如下,但这种方法无疑不太优雅且缺少考虑。

/**
* 下载预览区域为图片的业务逻辑钩子
*/
const useDownload = (el) => {
	const [temp, setTemp] = useState(0);

	useEffect(() => {
		setTemp(temp+1);  // temp state 更新 => 重走一遍函数作用域
	}, [])
	
	// other logics
}

当然也有更加有效和优雅的解决方案,直接上代码。

/**
* 下载预览区域为图片的业务逻辑钩子
*/
const useDownload = (reviewRef) => {
  const onClick = () => {
	const el = reviewRef.current;
    el &&
      html2canvas(el).then((canvas) => {
        downloadByB64({
          fileName: "report.jpg",
          b64: `${canvas.toDataURL()}`,
        })
      })
  }

  return onClick
}

const App = () => {
  const reviewRef = useRef(null)
  const onClick = useDownload(reviewRef)

  return (
    <div>
      <button onClick={onClick}>下载图片</button>
      {/* 以下是预览区域 */}
      <article ref={reviewRef}>
        <h1>标题</h1>
        <p>内容</p>
      </article>
    </div>
  )
}

React 作用的时序并没有变,变化的是传给自定义hook 的参数,参数变成了完整 reviewRef 对象,而非精摘出来的 reviewRef.current 值,当 onClick 回调被执行时,onClick 函数作用域在 App 函数作用域上产生了闭包,读取到的 reviewRef.current 是符合预期的 DOM 元素的对象引用。

到此为止我们已经可以呼应到本文的主题了,ref 数据为什么设置成对象的形式?DOM 元素为什么要通过 ref.current 点用?

因为 dom 元素并非一开始就绑定在 ref 数据上,而是在组件渲染完成后才绑定在 ref 数据上,那么在不同作用域的传递数据时,使用 JavaScript object 的形式能够确保不同作用域读取的数据来自同一处内存块,尽管内存块中的数据内容在更新,但只要保证内存块的地址不变,就可以始终保证通过地址引用拿到的内存块的数据内容永远是最新的。以此延伸到在一些别的场景下我们也可以通过将函数形参传递成object形式以规避维护数据一致性的额外工作。

Pasted image 20221207122016.png

或许我们还可以把 useDownload hook 抽取得更加优雅,将 ref 数据的声明直接从 App 函数作用域移至 useDownload 函数作用域使UI跟逻辑分离得更彻底。

/**
 * 下载预览区域为图片的业务逻辑钩子
 */
const useDownload = () => {
  const reviewRef = useRef(null)
  const onClick = () => {
    const el = reviewRef.current
    el &&
      html2canvas(el).then((canvas) => {
        downloadByB64({
          fileName: "report.jpg",
          b64: `${canvas.toDataURL()}`,
        })
      })
  }
  return {
    onClick,
    reviewRef,
  }
}

const App = () => {
  const { onClick, reviewRef } = useDownload()

  return (
    <div>
      <button onClick={onClick}>下载图片</button>
      {/* 以下是预览区域 */}
      <article ref={reviewRef}>
        <h1>标题</h1>
        <p>内容</p>
      </article>
    </div>
  )
}

有没有同学跟我一样记性不好?🥹

既然上文已经说过,ref 数据看起来就是提供了一层对象包装,使数据在传递的过程中只传递对象引用而非传递 primitive values,那么是否有同学会和我一下本能地并不是特别钟意使用太多框架提供的方法,心里总觉得会加重一些记忆负担。提供的替代 ref 方案是在 useDownload 作用域的上层作用域声明一个 类ref 数据,提供代码如下。当然你会得到一个 React-warning 或者无法通过类型检查如果你使用 typescript 进行开发。

/**
 * 下载预览区域为图片的业务逻辑钩子 useDownload.js
 */
const refEqual = {};  // 类 ref 数据
const useDownload = () => {
  const onClick = () => {
    const el = refEqual.current
    el &&
      html2canvas(el).then((canvas) => {
        downloadByB64({
          fileName: "report.jpg",
          b64: `${canvas.toDataURL()}`,
        })
      })
  }
  return {
    onClick,
    refEqual
  }
}
/**
 * App UI 组件 App.jsx
 */
const App = () => {
  const { onClick } = useDownload()

  return (
    <div>
      <button onClick={onClick}>下载图片</button>
      {/* 以下是预览区域 */}
      <article ref={refEqual}>
        <h1>标题</h1>
        <p>内容</p>
      </article>
    </div>
  )
}

总结

  1. ref 的数据结构设计成对象的原因在于让数据在其他作用域中也能被正确地读取
  2. 在自定义hook的时候需要考虑到 React 运作时序,可能不能单单用常规的抽象函数的思维来抽象自定义hook

完整的代码示例请参阅 codesandbox 链接🔗 => why ref is object

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

登录 后参与评论
0 条评论

相关文章

  • React的ref是怎样调用其他元素?

    ref 可以理解为指向React 元素的变量,方便其他组件访问这个React元素。

    Learn-anything.cn
  • 问:React的setState为什么是异步的?

    不知道大家有没有过这个疑问,React 中 setState() 为什么是异步的?我一度认为 setState() 是同步的,知道它是异步的之后很是困惑,甚至期...

    beifeng1996
  • React中的setState为什么是异步的?

    不知道大家有没有过这个疑问,React 中 setState() 为什么是异步的?我一度认为 setState() 是同步的,知道它是异步的之后很是困惑,甚至期...

    beifeng1996
  • React篇(006)-React 很多个 setState 为什么是执行完再 render

    答案:react为了提高整体的渲染性能,会将一次渲染周期中的state进行合并,在这个渲染周期中对所有setState的所有调用都会被合并起来之后,再一次性的渲...

    齐丶先丶森
  • 为什么Java字符串是不可变对象?

    本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Java的String类是不可变对象?让我们一起来分析一下。

    java思维导图
  • 为什么Java字符串是不可变对象?

    本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Java的String类是不可变对象?让我们一起来分析一下。

    哲洛不闹
  • jdk源码分析之HashMap--为什么key不建议是可变对象

    接着之前的文章,我们死磕HashMap的每一个细节和用法。我们都知道创建HashMap的时候如果不指定类型,默认是HashMap<Object,Obje...

    叔牙
  • 渐进式React源码解析-实现Ref Api

    文章中涉及到的知识都是渐进式的讲解开发,当然如果对之间内容不感兴趣(已经了解),也可以直接切入本文内容,每一个章节都和之前不会有很强的耦合。

    19组清风
  • react源码解析5.jsx&核心api

    一句话概括就是,用js对象表示dom信息和结构,更新时重新渲染更新后的对象对应的dom,这个对象就是React.createElement()的返回结果

    zz1998
  • react源码解析--jsx&核心api

    一句话概括就是,用js对象表示dom信息和结构,更新时重新渲染更新后的对象对应的dom,这个对象就是React.createElement()的返回结果

    长腿程序员165858
  • 关于ref的一切

    所以,React需要持续追踪当前render的组件。这会让React在性能上变慢。

    公众号@魔术师卡颂
  • react源码解析5.jsx&核心api

    一句话概括就是,用js对象表示dom信息和结构,更新时重新渲染更新后的对象对应的dom,这个对象就是React.createElement()的返回结果

    用户9002110
  • react源码解析5.jsx&核心api

    一句话概括就是,用js对象表示dom信息和结构,更新时重新渲染更新后的对象对应的dom,这个对象就是React.createElement()的返回结果

    全栈潇晨
  • React 原理问题

    useEffect会捕获props和state。所以即便在回调函数里,你拿到的还是初始的props和state。如果想得到“最新”的值,可以使用ref。

    愤怒的小鸟
  • React高级特性解析

    使用API React.createContext  返回的是组件对象 可以利用结构的方式

    憧憬博客
  • 141. 精读《useRef 与 createRef 的区别》

    useRef 是常用的 API,但还有一个 createRef 的 API,你知道他们的区别吗?通过 React.useRef and React.create...

    黄子毅

扫码关注腾讯云开发者

领取腾讯云代金券