>首页> IT >

带你了解React中的Ref,值得了解的知识点分享

时间:2022-03-22 11:31:53       来源:转载
本篇文章带大家了解一下React中的Ref,介绍一些关于 Ref 你需要知道的知识点,希望对大家有所帮助!

Intro

在 React 项目中,有很多场景需要用到 Ref。例如使用 ref属性获取 DOM 节点,获取 ClassComponent 对象实例;用 useRefHook 创建一个 Ref 对象,以便解决像 setInterval获取不到最新的 state 的问题;你也可以调用 React.createRef方法手动创建一个 Ref对象。【相关推荐:Redis视频教程】

虽然 Ref用起来也很简单,但在实际项目中实战还是难免遇到问题,这篇文章将从源码的角度出发梳理各种和 Ref相关的问题,理清和 ref相关的 API 背后都干了什么。看完这篇文章或许可以让你对的 Ref有更深入地认识。

Ref 相关的类型声明

首先 refreference的简称,也就是引用。在 react的类型声明文件中,可以找到好几个和 Ref 相关的类型,这里将它们一一列举出来。

RefObject/MutableRefObject

interface RefObject { readonly current: T | null; }interface MutableRefObject { current: T; }

使用 useRefHook 的时候返回的就是 RefObject/MutableRefObejct,这两个类型都是定义了一个 { current: T }的对象结构,区别是 RefObject的 current 属性是只读的,如果修改 refObject.current,Typescript 会警告⚠️。

const ref = useRef(null)ref.current = "" // Error

TS 报错:无法分配到 "current" ,因为它是只读属性。

查看 useRef方法的定义,这里用了函数重载,当传入的泛型参数 T不包含 null时返回RefObject,当包含 null时将返回 MutableRefObject

function useRef(initialValue: T): MutableRefObject;function useRef(initialValue: T | null): RefObject;

所以如果你希望创建的 ref 对象 current 属性是可修改的,需要加上 | null

const ref = useRef(null)ref.current = "" // OK

调用 React.createRef()方法时返回的也是一个 RefObject

createRef

export function createRef(): RefObject {  const refObject = {    current: null,  };  if (__DEV__) {    Object.seal(refObject);  }  return refObject;}

RefObject/MutableRefObject是在 16.3版本才新增的,如果使用更早的版本,需要使用 Ref Callback

RefCallback

使用 Ref Callback就是传递一个回调函数,react 回调时会将对应的实例回传过来,可以自行保存以便调用。这个回调函数的类型就是 RefCallback

type RefCallback = (instance: T | null) => void;

使用 RefCallback示例:

import React from "react"export class CustomTextInput extends React.Component {  textInput: HTMLInputElement | null = null;  saveInputRef = (element: HTMLInputElement | null) => {    this.textInput = element;  }  render() {    return (          );  }}

Ref/LegacyRef

在类型声明中,还有 Ref/LegacyRef 类型,它们用于泛指 Ref 类型。 LegacyRef是兼容版本,在之前的老版本 ref还可以是 字符串。

type Ref = RefCallback | RefObject | null;type LegacyRef = string | Ref;

理解了和 Ref 相关的类型,写起 Typescript 来才能更得心应手。

Ref 的传递

特殊的 props

在 JSX 组件上使用 ref时,我们是通过给 ref属性设置一个 Ref。我们都知道 jsx的语法,会被 Babel 等工具编译成 createElement的形式。

// jsx// compiled toReact.createElement(App, {  ref: ref,  id: "my-app"});

看起来 ref和其他 prop 没啥区别,不过如果你尝试在组件内部打印 props.ref 却是 undefined。并且 dev环境控制台会给出提示。

React 对 ref 做了啥?在 ReactElement 源码中可以看到,refRESERVED_PROPS,同样有这种待遇的还有 key,它们都会被特殊处理,从 props 中提取出来传递给 Element

const RESERVED_PROPS = {  key: true,  ref: true,  __self: true,  __source: true,};

所以 ref是会被特殊处理的 “props“

forwardRef

16.8.0版本之前,Function Component 是无状态的,只会根据传入的 props render。有了 Hook 之后不仅可以有内部状态,还可以暴露方法供外部调用(需要借助 forwardRefuseImperativeHandle)。

如果直接对一个 Function Componentref,dev 环境下控制台会告警,提示你需要用 forwardRef进行包裹起来。

function Input () {    return }const ref = useRef()

forwardRef为何物?查看源码 ReactForwardRef.js 将 __DEV__相关的代码折叠起来,它只是一个无比简单的高阶组件。接收一个 render 的 FunctionComponent,将它包裹一下定义 $$typeofREACT_FORWARD_REF_TYPEreturn回去。

跟踪代码,找到 resolveLazyComponentTag,在这里 $$typeof会被解析成对应的 WorkTag。

REACT_FORWARD_REF_TYPE对应的 WorkTag 是 ForwardRef。紧接着 ForwardRef 又会进入 updateForwardRef 的逻辑。

case ForwardRef: {  child = updateForwardRef(    null,    workInProgress,    Component,    resolvedProps,    renderLanes,  );  return child;}

这个方法又会调用 renderWithHooks 方法,并在第五个参数传入 ref

nextChildren = renderWithHooks(  current,  workInProgress,  render,  nextProps,  ref, // 这里  renderLanes,);

继续跟踪代码,进入 renderWithHooks 方法,可以看到,ref会作为 Component的第二个参数传递。到这里我们可以理解被 forwardRef包裹的 FuncitonComponent第二个参数 ref是从哪里来的(对比 ClassComponent contructor 第二个参数是 Context)。

了解如何传递 ref,那下一个问题就是 ref 是如何被赋值的。

ref 的赋值

打断点(给 ref 赋值一个 RefCallback,在 callback 里面打断点) 跟踪到代码 commitAttachRef,在这个方法里面,会判断 Fiber 节点的 ref 是 function还是 RefObject,依据类型处理 instance。如果这个 Fiber 节点是 HostComponent (tag = 5) 也就是 DOM 节点,instance 就是该 DOM 节点;而如果该 Fiber 节点是 ClassComponent (tag = 1),instance 就是该对象实例。

function commitAttachRef(finishedWork) {  var ref = finishedWork.ref;  if (ref !== null) {    var instanceToUse = finishedWork.stateNode;    if (typeof ref === "function") {      ref(instanceToUse);    } else {      ref.current = instanceToUse;    }  }}

以上是 HostComponent 和 ClassComponent 中对 ref 的赋值逻辑,对于 ForwardRef 类型的组件走的是另外的代码,但行为基本是一致的,可以看这里 imperativeHandleEffect。

接下里,我们继续挖掘 React 源码,看看 useRef 是如何实现的。

useRef 的内部实现

通过跟踪代码,定位到 useRef 运行时的代码 ReactFiberHooks

这里有两个方法,mountRefupdateRef,顾名思义就是对应 Fiber节点 mountupdate时对 ref的操作。

function updateRef(initialValue: T): {|current: T|} {  const hook = updateWorkInProgressHook();  return hook.memoizedState;}function mountRef(initialValue: T): {|current: T|} {  const hook = mountWorkInProgressHook();  const ref = {current: initialValue};  hook.memoizedState = ref;  return ref;}

可以看到 mount时,useRef创建了一个 RefObject,并将它赋值给 hookmemoizedStateupdate时直接将它取出返回。

不同的 Hook memoizedState 保存的内容不一样,useState中保存 state信息, useEffect中 保存着 effect对象,useRef中保存的是 ref对象...

mountWorkInProgressHookupdateWorkInProgressHook方法背后是一条 Hooks 的链表,在不修改链表的情况下,每次 render useRef 都能取回同一个 memoizedState 对象,就这么简单。

应用:合并 ref

至此,我们了解了在 React 中 ref的传递和赋值逻辑,以及 useRef相关的源码。用一个应用题来巩固以上知识点:有一个 Input 组件,在组件内部需要通过 innerRef HTMLInputElement来访问 DOM节点,同时也允许组件外部 ref 该节点,需要怎么实现?

const Input = forwardRef((props, ref) => {  const innerRef = useRef(null)  return (      )})

考虑一下上面代码中的 ???应该怎么写。

============ 答案分割线 ==============

通过了解 Ref 相关的内部实现,很明显我们这里可以创建一个 RefCallback,在里面对多个 ref进行赋值就可以了。

export function combineRefs(  refs: Array | RefCallback>): React.RefCallback {  return value => {    refs.forEach(ref => {      if (typeof ref === "function") {        ref(value);      } else if (ref !== null) {        ref.current = value;      }    });  };}const Input = forwardRef((props, ref) => {  const innerRef = useRef(null)  return (      )})

更多编程相关知识,请访问:编程入门!!

以上就是带你了解React中的Ref,值得了解的知识点分享的详细内容,更多请关注php中文网其它相关文章!

关键词: 带你了解 回调函数 对大家有