一步一步搞懂react的createRef和forwardRef
最近在使用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節點 header
, ClassCom
是個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;
我們可以得出下面的結論
- 原生node節點,current指向該節點
- class類react組件,current指向該組件實例
- 函數式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這個名字了。
完美!