Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >React ref 的前世今生

React ref 的前世今生

作者头像
Nealyang
发布于 2019-09-29 08:42:50
发布于 2019-09-29 08:42:50
89800
代码可运行
举报
文章被收录于专栏:全栈前端精选全栈前端精选
运行总次数:0
代码可运行

作者:淡苍 原文链接:https://zhuanlan.zhihu.com/p/40462264

众所周知,React 通过声明式的渲染机制把复杂的 DOM 操作抽象成为简单的 state 与 props 操作,一时圈粉无数,一夜间将前端工程师从面条式的 DOM 操作中拯救出来。尽管我们一再强调在 React 开发中尽量避免 DOM 操作,但在一些场景中仍然无法避免。当然 React 并没有把路堵死,它提供了 ref 用于访问在 render 方法中创建的 DOM 元素或者是 React 组件实例。

ref 的三驾马车

在 React v16.3 之前,ref 通过字符串(string ref)或者回调函数(callback ref)的形式进行获取,在 v16.3 中,经 0017-new-create-ref 提案引入了新的 React.createRef API

注意:本文以下代码示例以及源码均基于或来源于 React v16.3.2 release 版本。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// string ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.myRef.focus();
  }
  render() {
    return <input ref="myRef" />;
  }
}

// callback ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.myRef.focus();
  }
  render() {
    return <input ref={(ele) => {
      this.myRef = ele;
    }} />;
  }
}

// React.createRef
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  componentDidMount() {
    this.myRef.current.focus();
  }
  render() {
    return <input ref={this.myRef} />;
  }
}

string ref 之殇

在 React.createRef 出现之前,string ref 就已被诟病已久,React 官方文档直接提出 string ref 将会在未来版本被移出,建议用户使用 callback ref 来代替,为何需要这么做呢?主要原因集中于以下几点:

  • 当 ref 定义为 string 时,需要 React 追踪当前正在渲染的组件,在 reconciliation 阶段,React Element 创建和更新的过程中,ref 会被封装为一个闭包函数,等待 commit 阶段被执行,这会对 React 的性能产生一些影响。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function coerceRef(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
) {
  ...
  const stringRef = '' + element.ref;
  // 从 fiber 中得到实例
  let inst = ownerFiber.stateNode;

  // ref 闭包函数
  const ref = function(value) {
    const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
    if (value === null) {
      delete refs[stringRef];
    } else {
      refs[stringRef] = value;
    }
  };
  ref._stringRef = stringRef;
  return ref;
  ...
}
  • 当使用 render callback 模式时,使用 string ref 会造成 ref 挂载位置产生歧义。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class MyComponent extends Component {
  renderRow = (index) => {
    // string ref 会挂载在 DataTable this 上
    return <input ref={'input-' + index} />;

    // callback ref 会挂载在 MyComponent this 上
    return <input ref={input => this['input-' + index] = input} />;
  }

  render() {
    return <DataTable data={this.props.data} renderRow={this.renderRow} />
  }
}
  • string ref 无法被组合,例如一个第三方库的父组件已经给子组件传递了 ref,那么我们就无法再在子组件上添加 ref 了,而 callback ref 可完美解决此问题。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/** string ref **/
class Parent extends React.Component {
  componentDidMount() {
    // 可获取到 this.refs.childRef
    console.log(this.refs);
  }
  render() {
    const { children } = this.props;
    return React.cloneElement(children, {
      ref: 'childRef',
    });
  }
}

class App extends React.Component {
  componentDidMount() {
    // this.refs.child 无法获取到
    console.log(this.refs);
  }
  render() {
    return (
      <Parent>
        <Child ref="child" />
      </Parent>
    );
  }
}

/** callback ref **/
class Parent extends React.Component {
  componentDidMount() {
    // 可以获取到 child ref
    console.log(this.childRef);
  }
  render() {
    const { children } = this.props;
    return React.cloneElement(children, {
      ref: (child) => {
        this.childRef = child;
        children.ref && children.ref(child);
      }
    });
  }
}

class App extends React.Component {
  componentDidMount() {
    // 可以获取到 child ref
    console.log(this.child);
  }
  render() {
    return (
      <Parent>
        <Child ref={(child) => {
          this.child = child;
        }} />
      </Parent>
    );
  }
}
  • 在根组件上使用无法生效。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ReactDOM.render(<App ref="app" />, document.getElementById('main')); 
  • 对于静态类型较不友好,当使用 string ref 时,必须显式声明 refs 的类型,无法完成自动推导。
  • 编译器无法将 string ref 与其 refs 上对应的属性进行混淆,而使用 callback ref,可被混淆。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/** string ref,无法混淆 */
this.refs.myRef
<div ref="myRef"></div>

/** callback ref, 可以混淆 */
this.myRef
<div ref={(dom) => { this.myRef = dom; }}></div>

this.r
<div ref={(e) => { this.r = e; }}></div>

createRef vs callback ref

对比新的 createRef 与 callback ref,并没有压倒性的优势,只是希望成为一个便捷的特性,在性能上会会有微小的优势,callback ref 采用了组件 render 过程中在闭包函数中分配 ref 的模式,而 createRef 则采用了 object ref。

createRef 显得更加直观,类似于 string ref,避免了 callback ref 的一些理解问题,对于 callback ref 我们通常会使用内联函数的形式,那么每次渲染都会重新创建,由于 react 会清理旧的 ref 然后设置新的(见下图,commitDetachRef -> commitAttachRef),因此更新期间会调用两次,第一次为 null,如果在 callback 中带有业务逻辑的话,可能会出错,当然可以通过将 callback 定义成类成员函数并进行绑定的方式避免。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class App extends React.Component {
  state = {
    a: 1,
  };

  componentDidMount() {
    this.setState({
      a: 2,
    });
  }

  render() {
    return (
      <div ref={(dom) => {
        // 输出 3 次
        // <div data-reactroot></div>
        // null
        // <div data-reactroot></div>
        console.log(dom);
      }}></div>
    );
  }
}

class App extends React.Component {
  state = {
    a: 1,
  };

  constructor(props) {
    super(props);
    this.refCallback = this.refCallback.bind(this);
  }

  componentDidMount() {
    this.setState({
      a: 2,
    });
  }

  refCallback(dom) {
    // 只输出 1 次
    // <div data-reactroot></div>
    console.log(dom);
  }

  render() {
    return (
      <div ref={this.refCallback}></div>
    );
  }
}

不过不得不承认,createRef 在能力上仍逊色于 callback ref,例如上一节提到的组合问题,createRef 也是无能为力的。在 React v16.3 中,string ref/callback ref 与 createRef 的处理略有差别,让我们来看一下 ref 整个构建流程。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// markRef 前会进行新旧 ref 的引用比较
if (current.ref !== workInProgress.ref) {
  markRef(workInProgress);
}

// effectTag 基于位操作,其中有 ref 的变更标志位
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

// effectTag 与 Ref 的 & 操作表示当前 fiber 有 ref 变更
if (effectTag & Ref) {
  commitAttachRef(nextEffect);
}

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      // 当前 Host 环境为 DOM 环境,HostComponent 即为 DOM 元素,需要借助实例获取原生 DOM 元素
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      // 对于 ClassComponent 等而言,直接返回实例即可
      default:
        instanceToUse = instance;
    }
    // string ref 与 callback 都会去执行 ref 闭包函数
    // createRef 会直接挂在 object ref 的 current 上
    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}

以上会涉及 react fiber 的一些概念与细节,比如:fiber 对象含义,fiber tree 构建更新过程,effectTag 的含义与收集过程等等,如果读者对上述细节不熟悉,可暂时跳过此段内容,不影响对于 ref 的掌握与理解。

穿云箭 React.forwardRef

除了 createRef 以外,React16 还另外提供了一个关于 ref 的 API React.forwardRef,主要用于穿过父元素直接获取子元素的 ref。在提到 forwardRef 的使用场景之前,我们先来回顾一下,HOC(higher-order component)在 ref 使用上的问题,HOC 的 ref 是无法通过 props 进行传递的,因此无法直接获取被包裹组件(WrappedComponent),需要进行中转。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function HOCProps(WrappedComponent) {
  class HOCComponent extends React.Component {
    constructor(props) {
      super(props);
      this.setWrappedInstance = this.setWrappedInstance.bind(this);
    }

    getWrappedInstance() {
      return this.wrappedInstance;
    }

    // 实现 ref 的访问
    setWrappedInstance(ref) {
      this.wrappedInstance = ref;
    }

    render() {
      return <WrappedComponent ref={this.setWrappedInstance} {...this.props} />;
    }
  }

  return HOCComponent;
}

const App = HOCProps(Wrap);

<App ref={(dom) => {
  // 只能获取到 HOCComponent
  console.log(dom);
  // 通过中转后可以获取到 WrappedComponent
  console.log(dom.getWrappedInstance());
}} />

在拥有 forwardRef 之后,就不需要再通过 getWrappedInstance 了,利用 forwardRef 能直接穿透 HOCComponent 获取到 WrappedComponent。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function HOCProps(WrappedComponent) {
  class HOCComponent extends React.Component {
    render() {
      const { forwardedRef, ...rest } = this.props;
      return <WrappedComponent ref={forwardedRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => {
    return <HOCComponent forwardedRef={ref} {...props}  />;
  });
}

const App = HOCProps(Wrap);

<App ref={(dom) => {
  // 可以直接获取 WrappedComponent
  console.log(dom);
}} />

React.forwardRef 的原理其实非常简单,forwardRef 会生成 react 内部一种较为特殊的 Component。当进行创建更新操作时,会将 forwardRef 组件上的 props 与 ref 直接传递给提前注入的 render 函数,来生成 children。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const nextChildren = render(workInProgress.pendingProps, workInProgress.ref);

React refs 到此就全部介绍完了,在 React16 新版本中,新引入了 React.createRef 与 React.forwardRef 两个 API,有计划移除老的 string ref,使 ref 的使用更加便捷与明确。如果你的应用已经升级到 React16.3+ 版本,那就放心大胆使用 React.createRef 吧,如果暂时没有的话,建议使用 callback ref 来代替 string ref。

我们团队目前正在深入研究 React16,欢迎社区小伙伴和我们一起探讨与前行,如果想加入我们,欢迎私聊或投递简历到 dancang.hj@alibaba-inc.com。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈前端精选 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
编辑精选文章
换一批
【React】:Refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
WEBJ2EE
2020/04/07
1.1K0
浅谈 React Refs
在React组件中,props是父组件与子组件的唯一通信方式,但是在某些情况下我们需要在props之外强制修改子组件或DOM元素,这种情况下React提供了Refs解决
IMWeb前端团队
2019/12/03
1K0
浅谈 React Refs
React 进阶 - Ref
ref 对象就是用 createRef 或者 useRef 创建出来的对象,一个标准的 ref 对象应该是如下的样子:
Cellinlab
2023/05/17
1.8K0
React 进阶 - Ref
关于ref的一切
所以,React需要持续追踪当前render的组件。这会让React在性能上变慢。
公众号@魔术师卡颂
2020/09/10
9020
React 中refs的使用方法和步骤
在 React 中,ref 是一种用于访问组件或 DOM 元素的引用的特殊属性。在组件中存储对 DOM 节点或组件实例的引用,直接访问和操作
王小婷
2023/10/10
4500
React的三大属性之refs的一些简单理解
React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。
henu_Newxc03
2021/12/26
8730
React 16.3新API
之前也有context,相当于自动向下传递的props,子树中的任意组件都可以从context中按需取值(配合contextTypes声明)
ayqy贾杰
2019/06/12
1.1K0
React 16.3新API
【React】你想知道的关于 Refs 的知识都在这了
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
Nealyang
2019/11/08
3.1K0
React Ref 使用总结
useRef 还可以传入一个初始值,这个值会保存在 ref.current 中,上面代码中,如果不给 div 元素传递 ref={divRef},则 divRef.current 的值将是我们传入的初始值。
多云转晴
2020/09/08
7.1K0
React Ref 使用总结
小结React(三):state、props、Refs
在React中state、props、Refs都是最基础的概念,本文将同时梳理下这三个知识点,主要内容包括:
前端林子
2019/05/02
7.5K1
小结React(三):state、props、Refs
React16 新特性
于 2017.09.26 Facebook 发布 React v16.0 版本,时至今日已更新到 React v16.6,且引入了大量的令人振奋的新特性,本文章将带领大家根据 React 更新的时间脉络了解 React16 的新特性。
前端迷
2019/08/27
1.2K0
React v16 新特性实践
我们在对以上新特性经过一段时间的使用过后,通过本文进行一些细节分享和总结。
QQ音乐技术团队
2018/05/29
1.9K8
React字符串形式的ref
在React中,我们可以使用字符串形式的ref来引用组件或DOM元素。字符串形式的ref是一种较早的ref使用方式,它允许我们通过字符串将ref与组件或DOM元素进行关联。
堕落飞鸟
2023/05/19
5840
React-组件-Ref转发
React中的Ref转发是一种强大的技术,用于在函数式组件中传递和访问DOM元素或子组件的引用。它允许你在函数组件中像类组件一样使用Ref,使代码更清晰和可维护。
杨不易呀
2023/09/30
3380
[OHIF-Viewers]医疗数字阅片-医学影像-REACT-React.createRef()-Refs and the DOM关于回调 refs 的说明
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
landv
2020/07/09
1.8K0
滴滴前端二面react面试题总结
Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:
goClient1992
2022/09/14
1.1K0
react面试题
调用setState之后发生了什么? 在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。 扩展1: setState的第一个参数除了对象,还能传什么? ---函数,参数为当前state
用户7162790
2022/03/23
7260
【React】243- 在 React 组件中使用 Refs 指南
本文通过四种方式来告诉你如何使用,虽然有一种被放弃了。今日早读文章由老虎集团@joking_zhang翻译授权分享。
pingan8787
2019/07/25
4.1K0
【React】243- 在 React 组件中使用 Refs 指南
React的ref是怎样调用其他元素?
ref 可以理解为指向React 元素的变量,方便其他组件访问这个React元素。
Learn-anything.cn
2021/11/28
1K0
React 16 新特性全解(上)
本次系列分上下两篇文章,上主要介绍从v16.0~ 16.4的新特性,下主要介绍16.5~16.8。下面就开始吧~
前端迷
2019/07/12
1.6K0
React 16 新特性全解(上)
相关推荐
【React】:Refs
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验