Ref
学完React的初级阶段对Ref的感受是用于获取真实DOM的阶段,以及组件实例化。但是Ref有很多其他的用处,下面将分析React Ref的基础用法和高阶用法,明白React内部如何处理Ref以及Ref的原理。
Ref的概念及其基本使用
第一步需要分清的是Ref对象的创建和对Ref对象的处理,原始Ref对象是由React.createRef 或者 React.useRef 来创建的,而React对Ref对象的处理主要是通过标签中ref属性。
Ref对象的创建
ref对象:
{
current:null , // current指向ref对象获取到的实际内容,可以是dom元素,组件实例等等。
}
-
React.createRef
export function createRef() { const refObject = { current: null, } return refObject; } 所做操作看源码就会发现很简单,创建了一个对象,然后对象的current属性保存了通过ref获取到的DOM元素或者组件实例等,createRef创建出来的Ref对象会绑定到组件实例上面,这也是函数式组件不能使用createRef的原因,会导致Ref对象的内容丢失。
-
React.useRef
其实两者所做的事情大差不差,主要就是上述提到的问题,createRef创建出来的Ref对象是挂载到组件实例上面的,虽然函数式组件也有组件实例,如果强行挂载每次组件的更新都是函数的重新执行这是又将重新创建Ref对象,所以就会造成Ref对象内容的丢失。那么该如何解决这个问题呢?在之前就有提到过,每一个React Element都会在创建时对应出一个fiber对象,而这个对象仅有在该element销毁时才会消失,所以函数式组件可以考虑将Ref对象挂载到fiber对象上,这也就是useRef所做的事情,其他的和createRef相似。
React对ref标签属性的处理
类组件获取Ref有三种方式:
-
ref属性是一个字符串
/* 类组件 */ class Children extends Component{ render=()=><div>hello,world</div> } /* TODO: Ref属性是一个字符串 */ export default class Index extends React.Component{ componentDidMount(){ console.log(this.refs) } render=()=> <div> <div ref="currentDom" >字符串模式获取元素或组件</div> <Children ref="currentComInstance" /> </div> }在这段代码中,Index 组件使用了 ref 属性来引用子组件 Children 和一个 DOM 元素。ref 属性的作用是用于引用组件或 DOM 元素。在这里,ref 属性被设置为字符串类型。在 componentDidMount 生命周期方法中,可以通过 this.refs 来访问被设置了 ref 的子组件和 DOM 元素。在最新版的 React 中(17.x),直接使用 ref 属性来引用组件或 DOM 元素已经被废弃,推荐React.createRef() 或者 useRef() 方法来创建一个 ref 对象。
结果:
{ currentDom: <div>字符串模式获取元素或组件</div>, currentComInstance: <Children>hello, world</Children> } -
ref属性是一个函数
class Children extends React.Component{ render=()=><div>hello,world</div> } /* TODO: Ref属性是一个函数 */ export default class Index extends React.Component{ currentDom = null currentComponentInstance = null componentDidMount(){ console.log(this.currentDom) console.log(this.currentComponentInstance) } render=()=> <div> <div ref={(node)=> this.currentDom = node } >Ref模式获取元素或组件</div> <Children ref={(node) => this.currentComponentInstance = node } /> </div> }这个地方可能会不太好理解,其实这个函数就是callback回调函数,当真实DOM创建时,这个DOM元素或组件实例将会以第一个参数的形式传进来,然后再赋值给currentDom和currentComponentInstance。
结果:
<div>Ref模式获取元素或组件</div> // this.currentDom <Children>hello, world</Children> // this.currentComponentInstance -
ref属性是一个Ref对象
这个相对来说就较为简单了
class Children extends React.Component{ render=()=><div>hello,world</div> } export default class Index extends React.Component{ currentDom = React.createRef(null) currentComponentInstance = React.createRef(null) componentDidMount(){ console.log(this.currentDom) console.log(this.currentComponentInstance) } render=()=> <div> <div ref={ this.currentDom } >Ref对象模式获取元素或组件</div> <Children ref={ this.currentComponentInstance } /> </div> }结果:
{ current: <div>Ref对象模式获取元素或组件</div> } // this.currentDom { current: <Children>hello, world</Children> } // this.currentComponentInstance
Ref高阶用法
forwordRef的使用
forwardRef 是一个 React 的高级组件(Higher-Order Component),用于在组件之间传递 ref 引用。通常情况下,当你在组件中使用 ref 属性时,ref 只会引用到组件的实例而不会传递到组件的子组件。但是,有时候你可能需要在一个高阶组件中引用到其包装的子组件的实例。这是forwordRef就能将ref传递到子组件当中。
-
// 孙组件 function Son (props){ const { grandRef } = props return <div> <div> i am alien </div> <span ref={grandRef} >这个是想要获取元素</span> </div> } // 父组件 class Father extends React.Component{ constructor(props){ super(props) } render(){ return <div> <Son grandRef={this.props.grandRef} /> </div> } } const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref} {...props} />) // 爷组件 class GrandFather extends React.Component{ constructor(props){ super(props) } node = null componentDidMount(){ console.log(this.node) // span #text 这个是想要获取元素 } render(){ return <div> <NewFather ref={(node)=> this.node = node } /> </div> } }注意看这个forwardRef,它将ref混入到了props中然后供孙组件去使用。
打印结果就是能够成功获取到这个span 元素。
-
// 表单组件 class Form extends React.Component{ render(){ return <div>{...}</div> } } // index 组件 class Index extends React.Component{ componentDidMount(){ const { forwardRef } = this.props forwardRef.current={ form:this.form, // 给form组件实例 ,绑定给 ref form属性 index:this, // 给index组件实例 ,绑定给 ref index属性 button:this.button, // 给button dom 元素,绑定给 ref button属性 } } form = null button = null render(){ return <div > <button ref={(button)=> this.button = button } >点击</button> <Form ref={(form) => this.form = form } /> </div> } } const ForwardRefIndex = React.forwardRef(( props,ref )=><Index {...props} forwardRef={ref} />) // home 组件 export default function Home(){ const ref = useRef(null) useEffect(()=>{ console.log(ref.current) },[]) return <ForwardRefIndex ref={ref} /> }这个也好理解,就是将home组件传过来的ref对象进行了扩充,往里面添加了新的属性,这些属性也对应着DOM节点或者是组件实例,多个ref的合并。
打印结果:
{ form: FormComponentInstance, // Form 组件的实例 index: IndexComponentInstance, // Index 组件的实例 button: buttonElement // button 元素的引用 } -
forwordRef还可以用于高阶组件当中,当父组件需要获取到初始的子组件时。
function HOC(Component){ class Wrap extends React.Component{ render(){ const { forwardedRef ,...otherprops } = this.props return <Component ref={forwardedRef} {...otherprops} /> } } return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) } class Index extends React.Component{ render(){ return <div>hello,world</div> } } const HocIndex = HOC(Index) export default ()=>{ const node = useRef(null) useEffect(()=>{ console.log(node.current) /* Index 组件实例 */ },[]) return <div><HocIndex ref={node} /></div> }
ref实现组件间的通信
-
类组件的通信
这种通信方式和props通信很像,并且添加了新的功能,父组件可以通过ref获取到子组件的实例,从而能够去调用子组件的一些方法去实现父子组件之间的通信。
/* 子组件 */ class Son extends React.PureComponent{ state={ fatherMes:'', sonMes:'' } fatherSay=(fatherMes)=> this.setState({ fatherMes }) /* 提供给父组件的API */ render(){ const { fatherMes, sonMes } = this.state return <div className="sonbox" > <div className="title" >子组件</div> <p>父组件对我说:{ fatherMes }</p> <div className="label" >对父组件说</div> <input onChange={(e)=>this.setState({ sonMes:e.target.value })} className="input" /> <button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) } >to father</button> </div> } } /* 父组件 */ export default function Father(){ const [ sonMes , setSonMes ] = React.useState('') const sonInstance = React.useRef(null) /* 用来获取子组件实例 */ const [ fatherMes , setFatherMes ] = React.useState('') const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */ return <div className="box" > <div className="title" >父组件</div> <p>子组件对我说:{ sonMes }</p> <div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) } className="input" /> <button className="searchbtn" onClick={toSon} >to son</button> <Son ref={sonInstance} toFather={setSonMes} /> </div> } -
函数式组件的通信
函数式组件由于没有实例所以这时候React Hooks就提供了另一个方法useImperativeHandle
useImperativeHandle 接受三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
// 子组件 function Son (props,ref) { const inputRef = useRef(null) const [ inputValue , setInputValue ] = useState('') useImperativeHandle(ref,()=>{ const handleRefs = { onFocus(){ /* 声明方法用于聚焦input框 */ inputRef.current.focus() }, onChangeValue(value){ /* 声明方法用于改变input的值 */ setInputValue(value) } } return handleRefs },[]) return <div> <input placeholder="请输入内容" ref={inputRef} value={inputValue} /> </div> } const ForwarSon = forwardRef(Son) // 父组件 class Index extends React.Component{ cur = null handerClick(){ const { onFocus , onChangeValue } =this.cur onFocus() // 让子组件的输入框获取焦点 onChangeValue('let us learn React!') // 让子组件input } render(){ return <div style={{ marginTop:'50px' }} > <ForwarSon ref={cur => (this.cur = cur)} /> <button onClick={this.handerClick.bind(this)} >操控子组件</button> </div> } }父组件可以调用ref下的onFocus 和 onChangeValue 控制子组件中 input 赋值和聚焦。
为什么要使用到forwardRef?
答:函数式组件没有实例,所以要用到forwardRef转发Ref。
函数式组件缓存数据
这个可能会不太好理解,牵扯到了后面的渲染控制性能优化,上面提到过,不管是类组件还是函数式组件,只要组件没有销毁那么绑定在其实例上或者fiber上的ref对象都不会消失,所以可以利用这个特点把一些不依赖视图更新的数据放到ref原始对象中,这样做有两个好处:
- 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
- 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。
const toLearn = [ { type: 1 , mes:'let us learn React' } , { type:2,mes:'let us learn Vue3.0' } ]
export default function Index({ id }){
const typeInfo = React.useRef(toLearn[0])
const changeType = (info)=>{
typeInfo.current = info /* typeInfo 的改变,不需要视图变化 */
}
useEffect(()=>{
if(typeInfo.current.type===1){
/* ... */
}
},[ id ]) /* 无须将 typeInfo 添加依赖项 */
return <div>
{
toLearn.map(item=> <button key={item.type} onClick={ changeType.bind(null,item) } >{ item.mes }</button> )
}
</div>
}
- 用一个 useRef 保存 type 的信息,type 改变不需要视图变化。
- 按钮切换直接改变 useRef 内容。
- useEffect 里面可以直接访问到改变后的 typeInfo 的内容,不需要添加依赖项。
Ref的原理
React是如何对ref标签进行处理的,下面看一个小Demo
export default class Index extends React.Component{
state={ num:0 }
node = null
render(){
return <div >
<div ref={(node)=>{
this.node = node
console.log('此时的参数是什么:', this.node )
}} >ref元素节点</div>
<button onClick={()=> this.setState({ num: this.state.num + 1 }) } >点击</button>
</div>
}
}
可以猜一下打印的结果会是什么?
结果:null;div;
为什么会出现打印两个结果呢?
这就要从它的处理时机和处理逻辑来分析。
ref处理时机和处理逻辑
之前的lifeCycle提到了更新的过程分为render阶段和commit阶段,对于Ref的处理全都是在commit的阶段发生,但是分别会有两个方法对Ref对象进行处理,分别是commitDetachRef 和 commitAttachRef ,上述两次 console.log 一次为 null,一次为div 就是分别调用了上述的方法。这两次正正好好,一次在 DOM 更新之前,一次在 DOM 更新之后。
-
第一阶段:一次更新中,在 commit 的 mutation 阶段, 执行commitDetachRef,commitDetachRef 会清空之前ref值,使其重置为 null。
function commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { if (typeof currentRef === 'function') { /* function 和 字符串获取方式。 */ currentRef(null); } else { /* Ref对象获取方式 */ currentRef.current = null; } } } -
第二阶段:DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。
-
第三阶段:layout 阶段,在更新真实元素节点之后,此时需要更新 ref 。
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: //元素节点 获取元素 instanceToUse = getPublicInstance(instance); break; default: // 类组件直接使用实例 instanceToUse = instance; } if (typeof ref === 'function') { ref(instanceToUse); //* function 和 字符串获取方式。 */ } else { ref.current = instanceToUse; /* ref对象方式 */ } } }ref对象方式:
node = React.createRef() <div ref={ node } ></div>或许到这里应该理解了上面提到的三种获取ref的方式,但是还有一个问题那就是为什么字符串获取方式会按照function的形式进行赋值,因为在React检测到ref属性是一个字符串的时候会自动绑定一个函数来处理ref。
const ref = function(value) { let refs = inst.refs; if (refs === emptyRefsObject) { refs = inst.refs = {}; } if (value === null) { delete refs[stringRef]; } else { refs[stringRef] = value; } }这个stringRef就是那个字符串。
Ref处理特性
是不是每一次fiber更新都会执行上述的两个函数ommitDetachRef 和 commitAttachRef来更新Ref呢?答案肯定是否定的。
更新Ref
-
commitDetachRef 调用时机
function commitMutationEffects(){ if (effectTag & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current); } } } -
commitAttachRef 调用时机
function commitLayoutEffects(){ if (effectTag & Ref) { commitAttachRef(nextEffect); } }
不难看出当有Ref tag时才会执行这两个函数,那么这个tag是何时被打上去的呢?又是咋打上去的呢?着就要提到另一个方法markRef专门用来帮Ref打tag。
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) || // 初始化的时候
(current !== null && current.ref !== ref) // ref 指向发生改变
) {
workInProgress.effectTag |= Ref;
}
}
不难看出markRef的执行时机:
- 第一种就是类组件的更新过程中。
- 第二种就是更新 HostComponent 的时候,比如 等元素。
markRef 会在以下两种情况下给 effectTag 标记 Ref,给Ref打标签。
- 第一种current === null && ref !== null:就是在 fiber 初始化的时候,第一次 ref 处理的时候,是一定要标记 Ref 的。
- 第二种current !== null && current.ref !== ref:就是 fiber 更新的时候,但是 ref 对象的指向变了。
卸载Ref
例子:
this.state.isShow && <div ref={()=>this.node = node} >元素节点</div>
当isShow为false时div元素将会被卸载,那么这个ref该如何去处理呢?
被卸载的 fiber 会被打成 Deletion effect tag ,然后在 commit 阶段会进行 commitDeletion 流程。对于有 ref 标记的 ClassComponent (类组件) 和 HostComponent (元素),会统一走 safelyDetachRef 流程,这个方法就是用来卸载 ref。
function safelyDetachRef(current) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') { // 函数式 | 字符串
ref(null)
} else {
ref.current = null; // ref 对象
}
}
}
这样就完成了ref的卸载。
总结
- Ref 对象的二种创建方式,以及三种获取 ref 方法。
- forwardRef 用法。
- ref 组件通信-函数组件和类组件两种方式。
- useRef 缓存数据。
- Ref 的处理逻辑原理。