引言
置信大部分同学对ref的认知还处于获取DOM
节点和组件实例层面上,实际上除了这个性能,还有其它小技巧能够应用,这篇文章将具体地介绍ref
的创立和应用,相干代码会以函数组件为主。
创立Ref
字符串类型Ref
class App extends React.Component { render() { console.log("this", this); return ( <> <div ref="dom">hello world</div> <Children ref="children" /> </> ); }}class Children extends React.Component { render() { return <div>china</div>; }}
打印后果
无论是对于实在DOM
,还是类组件,通过字符串创立的ref
会绑定在this.refs
对象上。
函数类型Ref
class App extends React.Component { dom = null children = null render() { componentDidMount() { console.log(this.dom, this.childrenDom); } return ( <> <div ref={(node) => this.dom = node}>hello world</div> <Children ref={(node) => this.childrenDom = node} /> </> ); }}class Children extends React.Component { render() { return <div>china</div>; }}
打印后果
对象类型Ref
createRef创立Ref
创建对象类型的Ref
对于类组件和函数组件的创立形式是不一样的,在类组件中创立Ref
对象须要通过createRef
函数来进行创立,这个函数的实现其实非常简单
function createRef() { return { current: null }}
从下面的代码来看,就是创立并返回带有current
属性的对象。
留神:不要在函数组件外面应用createRef函数
举个例子来看下为什么在函数组件外面不能应用createRef函数来创立Ref
function Counter() { const ref1 = React.useRef(0); const ref2 = React.createRef(); const [count, setCount] = React.useState(0); console.log(ref1, ref2); return ( <> <div class="counter-area"> <div class="count1">count1: {ref1.current}</div> <div class="count2">count2: {ref2.current}</div> </div> <button onClick={() => { ref1.current = (ref1.current || 0) + 1; ref2.current = (ref2.current || 0) + 1; setCount(count + 1); }} > 点我++ </button> </> );}
一起看下打印后果
发现count2
的右侧始终没有打印,依据控制台外面打印的数据不难发现由createRef
创立的ref
的current
属性始终都为null
,所以count2
右侧没有数据。
函数组件更新UI视图实际上会将整个函数从新执行一遍,那么createRef
函数在组件每次渲染的时候也会从新调用,生成初始状态的ref
,对应的current
属性为null
。
useRef创立Ref
React
提供了内置的useRef
来生成ref
对象。
function Counter() { const ref = React.useRef(null); React.useEffect(() => { console.log(ref.current); }, []); return <div ref={ref}>hello world</div>;}
下面有说过在函数组件外面通过createRef
创立ref
会有问题,那通过useRef
这个函数生成的ref
会不会有上述的问题?
答案是不会,在类组件外面能够把ref
存储到实例对象下来,然而函数组件并没有实例的说法,然而函数组件有对应的Fiber
对象,只有组件没有销毁,那么fiber
对象也不会销毁,将useRef
产生的ref
挂载到对应的fiber
对象上,那么ref
就不会在函数每次从新执行时被重置掉。
Ref的高阶用法
Ref转发
咱们在通过Props的形式向组件传递信息时,某些特定的属性是会被React
底层解决的,咱们在组件外面是无奈承受到的,例如key
、ref
function Counter(props) { const { key, ref } = props; console.log("props", props); return ( <> <div class="key">key: {key}</div> <div class="ref">ref: {ref.current}</div> </> );}function App() { const ref = useRef(null); return <Counter key={"hello"} ref={ref} />;}
控制台中打印信息如下,能够理解到通过props
的形式传递key
和ref
给组件是有效的。
那如何传递ref
和key
给子组件,既然react
不容许,那么咱们换个身份,暗度陈仓。
function Counter(props) { const { pkey, pref } = props; console.log("props", props); return ( <> <div class="key">key: {pkey}</div> <div class="ref">ref: {pref.current}</div> </> );}function App() { const ref = useRef(null); return <Counter pkey={"hello"} pref={ref} />;}
打印信息如下
通过别名的形式能够传递ref,那么为什么还须要forwardRef来传递Ref?
假如这样一种场景,你的组件中须要援用内部库提供的组件,例如antd
、fusion
这种组件库提供的组件,而且你须要传递ref
给这个组件,因为援用的内部组件是不可能晓得你会用什么样的别名属性来传递ref
,所以这个时候只能应用forwardRef
来进行传递ref
,因为这个是react
规定用来传递ref
值的属性名。
跨层级传递Ref
场景:须要把ref
从GrandFather
组件传递到Son
组件,在Son
组件外面展现ref
存储的信息并获取Son
组件外面的dom
节点。
const Son = (props) => { const { msg, grandFatherRef } = props; return ( <> <div class="msg">msg: {msg}</div> <div class="ref" ref={grandFatherRef}> ref: {grandFatherRef.current} </div> </> );};const Father = forwardRef((props, ref) => { return <Son grandFatherRef={ref} {...props} />;});function GrandFather() { const ref = useRef("我是来自GrandFather的ref啦~~"); useEffect(() => console.log(ref.current), []); return <Father ref={ref} msg={"我是来自GrandFather的音讯啦~~"} />;}
页面展现状况
控制台打印后果
下面的代码就是通过别名配合forwardRef
来转发ref
,这种转发ref
的形式,是十分常见的用法。通过别名的形式传递ref
和通过forwardRef
传递ref
的形式其实没有太大的差异,最实质的区别就是通过别名的形式须要传递方和接管方人为地都约定好属性名,而通过forwardRef
的形式是react
外面约定了传递的ref
属性名。
合并转发Ref
咱们从父组件传递上来的ref不仅仅能够用来展现获取某个dom
节点,还能够在子组件外面更改ref
的信息,获取子组件外面其它信息。
场景:咱们须要获取子组件外面input和button这两个dom
节点
const Son = forwardRef((props, ref) => { console.log("props", props); const sonRef = useRef({}); useEffect(() => { ref.current = { ...sonRef.current, }; return () => ref.current = {} }, []); return ( <> <button ref={(button) => Object.assign(sonRef.current, { button })}> 点我 </button> <input type="text" ref={(input) => Object.assign(sonRef.current, { input })} /> </> );});function Father() { const ref = useRef("我是来自Father的ref啦~~"); useEffect(() => console.log(ref.current), []); return <Son ref={ref} msg={"我是来自Father的音讯啦~~"} />;}
控制台打印后果
只管咱们传递上来的ref
的current
为字符串属性,然而咱们通过在子组件外面批改current
属性,进而获取到了子组件外面button
和input
的dom
节点
下面的代码其实能够把useEffect
更改成为**useImperativeHandle**
,这是react
提供的hook
,用来配合forwardRef
来进行应用,能够。
**useImperativeHandle**
接管三个参数:
- 通过
forwardRef
传递过去的ref
- 处理函数,其返回值裸露给父组件的
ref
对象 依赖项
const Son = forwardRef((props, ref) => { console.log("props", props); const sonRef = useRef({}); useImperativeHandle( ref, () => ({ ...sonRef.current, }), [] ); return ( <> <button ref={(button) => Object.assign(sonRef.current, { button })}> 点我 </button> <input type="text" ref={(input) => Object.assign(sonRef.current, { input })} /> </> );});function Father() { const ref = useRef("我是来自Father的ref啦~~"); useEffect(() => console.log(ref.current), []); return <Son ref={ref} msg={"我是来自Father的音讯啦~~"} />;}
高阶组件转发Ref
在应用高阶组件包裹一个原始组件的时候,因为高阶组件会返回一个新的组件,如果不进行ref转发,从下层组件传递下来的ref会指向这个新的组件
function HOC(Comp) { function Wrapper(props) { const { forwardedRef, ...restProps } = props; return <Comp ref={forwardedRef} {...restProps} />; } return forwardRef((props, ref) => ( <Wrapper forwardedRef={ref} {...props} /> ));}const Son = forwardRef((props, ref) => { return <div ref={ref}>hello world</div>;});const NewSon = HOC(Son);function Father() { const ref = useRef("我是来自Father的ref啦~~"); useEffect(() => console.log(ref.current), []); return <NewSon ref={ref} msg={"我是来自Father的音讯啦~~"} />;}
留神:第12行处减少
forwardRef
转发是因为函数组件无奈间接接管ref
属性
控制台打印后果
Ref实现组件间的通信
子组件能够通过props
获取和扭转父组件外面的state
,父组件也能够通过ref
来获取和扭转子组件外面的state
,实现了父子组件之间的双向通信,这其实在肯定水平上突破了react单向数据传递的准则。
const Son = forwardRef((props, ref) => { const { toFather } = props; const [fatherMsg, setFatherMsg] = useState(''); const [sonMsg, setSonMsg] = useState(''); useImperativeHandle( ref, () => ({ fatherSay: setFatherMsg }), [] ); return ( <div className="son-box"> <div className="father-say">父组件对我说: {fatherMsg}</div> <div className="son-say"> 我对父组件说: <input type="text" value={sonMsg} onChange={(e) => setSonMsg(e.target.value)} /> <button onClick={() => toFather(sonMsg)}>to father</button> </div> </div> );});function Father() { const [fatherMsg, setFatherMsg] = useState(''); const [sonMsg, setSonMsg] = useState(''); const sonRef = useRef(null); return ( <div className="father-box"> <div className="son-say">子组件对我说: {sonMsg}</div> <div className="father-say"> 对子组件说: <input type="text" value={fatherMsg} onChange={(e) => setFatherMsg(e.target.value)} /> <button onClick={() => sonRef.current.fatherSay(fatherMsg)}> to son </button> </div> <Son ref={sonRef} toFather={setSonMsg} /> </div> );}
Ref和State的抉择
咱们在平时的学习和工作中,在应用函数组件的时候,经常会应用useState
来生成state
,每次state
扭转都会引发整个函数组件从新渲染,然而有些state
的扭转不须要更新视图,那么咱们能够思考应用ref来存储,ref
存储的值在发生变化后是不会引起整个组件的从新渲染的。