最近在使用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() 方法封閉一個對象,阻止添加新屬性並將所有現有屬性標記爲不可配置。當前屬性的值只要原來是可寫的就可以改變。

我們簡單測試下

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;

我們可以得出下面的結論

  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()?
看來函數式組件也是有辦法使用像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;

現在我們就看不到警告,同時 this.state.menuRef.current 不再是null了,而是指向了組件對應的node節點 aside 了。跟直接使用在原生node上沒什麼區別。

仔細查看開發者我們會發現這個改造後的AsideMenu組件實際返回的是個匿名組件。

這樣的壞處是在調試工具中不好識別,有些團隊的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);

再看一下,現在好了,繼續用回Menu這個名字了。

完美!

相關文章