23

一步一步搞懂react的createRef和forwardRef

 3 years ago
source link: https://www.daozhao.com/9028.php
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近在使用react过程中发现在使用ref时的一些场景,自己初步感觉react的ref没有vue那么强大。

现在我就简单看下怎么使用ref?

createRef

我们直接看源码

// node_modules/react/umd/react.development.js

// an immutable object with a single mutable value
  function createRef() {
    var refObject = {
      current: null
    };

    {
      Object.seal(refObject);
    }

    return refObject;
  }

其实 createRef 也没做什么,就是返回了一个对象 { current: null} ,但是在返回值之前进行了 Object.seal 操作

Object.seal() 方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

我们简单测试下

2u2Qfyq.jpg!web

createRef使用场景

我们一般是在构造函数里面先新建个空的ref,为了方便在调试工具中查看,我们把ref放到state里面。

比如

import AsideMenu from './Menu.jsx';

export default class Layout extends React.Component {
    constructor() {
        this.state = {
            headerRef: createRef(),
            menuRef: createRef(),
            classRef: createRef(),
        }
    }
    render() {
        return (
            <>
                <header ref={this.state.headerRef} />
                <ClassCom ref={this.state.classRef} />
                <AsideMenu ref={this.state.menuRef} />
            </>
        )
    }
}

其中 header 是原生的node节点 headerClassCom 是个class类组件, AsideMenu 是函数式组件。

const Menu = (props) => (
  <aside id="menu" className={props.show ? 'show' : ''}>
    <div className="inner flex-row-vertical">
      <Profile avatarUrl="/owner.jpg"/>
      <div className="scroll-wrap flex-col">
        <MenuList asides={props.asides}/>
        <ArchiveList />
      </div>
    </div>
  </aside>
);
export default Menu;

v6jIfqJ.jpg!web

我们可以得出下面的结论

  1. 原生node节点,current指向该节点
  2. class类react组件,current指向该组件实例
  3. 函数式react组件,current仍然指向null。没错,就是新建时的null,未曾变更过。因为函数式组件没有实例嘛

同时我们可以在控制台看到这样的警告 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
zUjInuB.jpg!web 看来函数式组件也是有办法使用像class组件那样使用ref的,我们需要 forwardRef 的帮助

forwardRef

function forwardRef(render) {
    {
      if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
        error('forwardRef requires a render function but received a `memo` ' + 'component. Instead of forwardRef(memo(...)), use ' + 'memo(forwardRef(...)).');
      } else if (typeof render !== 'function') {
        error('forwardRef requires a render function but was given %s.', render === null ? 'null' : typeof render);
      } else {
        if (render.length !== 0 && render.length !== 2) {
          error('forwardRef render functions accept exactly two parameters: props and ref. %s', render.length === 1 ? 'Did you forget to use the ref parameter?' : 'Any additional parameter will be undefined.');
        }
      }

      if (render != null) {
        if (render.defaultProps != null || render.propTypes != null) {
          error('forwardRef render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?');
        }
      }
    }

    return {
      $$typeof: REACT_FORWARD_REF_TYPE,
      render: render
    };
  }

根据源码来看 forwardRef 也没有什么神操作,从返回值来看就是将 render 作为返回值的render属性,同时还会再加一个 $$typeof 属性,它的值是 REACT_FORWARD_REF_TYPE ,这显然是一个枚举值, $$typeof 属性应该是就是供其它地方检测用的。

当然在 forwardRef 方法返回值之前会做几个判断,里面其实也用到了 $$typeof 属性。

那我们参照官网的示例简单改造下我们AsideMenu组件。

const Menu = forwardRef((props, ref) => (
  <aside ref={ref} id="menu" className={props.show ? 'show' : ''}>
    <div className="inner flex-row-vertical">
      <Profile avatarUrl="/owner.jpg"/>
      <div className="scroll-wrap flex-col">
        <MenuList asides={props.asides}/>
        <ArchiveList />
      </div>
    </div>
  </aside>
));

export default Menu;

JbQf6jM.jpg!web 现在我们就看不到警告,同时 this.state.menuRef.current 不再是null了,而是指向了组件对应的node节点 aside 了。跟直接使用在原生node上没什么区别。

仔细查看开发者我们会发现这个改造后的AsideMenu组件实际返回的是个匿名组件。

URfUjyv.jpg!web

这样的坏处是在调试工具中不好识别,有些团队的eslint也会不允许使用匿名组件。

那我们再微调下。

const Menu = (props, ref) => (
  <aside ref={ref} id="menu" className={props.show ? 'show' : ''}>
    <div className="inner flex-row-vertical">
      <Profile avatarUrl="/owner.jpg"/>
      <div className="scroll-wrap flex-col">
        <MenuList asides={props.asides}/>
        <ArchiveList />
      </div>
    </div>
  </aside>
);
export default forwardRef(Menu);
a26Jne3.jpg!web

再看一下,现在好了,继续用回Menu这个名字了。

完美!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK