一、refs 的由来
什么是refs
refs是拿到真实的DOM节点和React元素实例的一种方法。在React官方文档中有提到Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。 React是单向的数据流,父子组件的交互是通过props。修改子组件需要使用新的props来重新渲染子组件。但是在某些情况下,你需要修改数据流以外的子组件,如DOM元素或者一个React元素实例,此时就需要Refs来改变。
refs的适用场景
根据官方文档,refs在以下几种场景中适用: 1. 操作DOM元素、控制文本内容或者媒体播放 2. 操作DOM元素,触发强制动画 3. 集成第三方DOM库
我们来看下,React中有以下四种使用方式
二、refs的四种方式
1. callback ref
React 支持一种通过回调的方式设置refs,这种方式可以控制refs何时被设置和解除。创建ref属性时,你会传递一个函数,这个函数会接受DOM元素或者组件实例作为参数,使他们能够在其他地方被访问。
class TestTemp extends React.Component {componentDidMount() {this.myRef.focus();}render() {return <input ref={(element) => {this.myRef = element;}} />;}
}
React在componetDidMount或者componetDidUpdate触发前调用ref回调,并且传入对应的DOM元素,保证refs是最新的。 在下面的例子中,父组件可以获取到子组件的DOM节点
function Child(props) {return (<div><input ref={props.myRef} /></div>);
}
class Parent extends React.Component {componentDidMount() {this.myElement.focus();}render() {return (<ChildmyRef={element => this.myElement = element}/>);}
}
父组件通过props传一个回调函数给子组件,子组件可以把这个函数作为ref属性的值绑定到DOM元素上。this.myElement就是对应子组件的DOM元素(input)
2. React.createRef()
createRef()是通过创建一个ref属性,传递给渲染的DOM元素或者是组件实例
class TestTemp extends React.Component {constructor(props) {super(props);this.myRef = React.createRef();}componentDidMount() {this.myRef.current.focus()}render() {return (<input ref={ this.myRef }></input>)}
}
在上面的例子中,创建一个ref属性实例myRef,并将其传给DOM元素 input中。之后对此元素的操作就可以在ref实例的current属性中处理。 对元素的操作根据节点类型的不同,ref的值也不同: 1. 当ref属性作用于一个HTML元素时,createRef()创建ref实例的current实例是底层DOM元素
class TestTemp extends React.Component {constructor(props) {super(props);// 创建一个ref存储 input元素this.myRef = React.createRef();}componentDidMount() {// 获取 input元素焦点this.myRef.current.focus()}render() {return (<div><input ref={this.myRef}></input></div>)}
}
2. 当ref属性作用于一个自定义组件时,createRef()创建ref实例的current实例是组件的挂载实例
class Children extends React.Component {constructor(props) {super(props);// 创建一个ref实例,存储input元素this.childRef = React.createRef();}childInput() {// 获取input焦点this.childRef.current.focus();}render(){return(<div><input type='text' ref={ this.childRef }/></div>)}
}
class Parent extends React.Component {constructor(props) {super(props);// 创建ref实例 存储Children 组件实例this.parentRef = React.createRef();}componentDidMount() {// 通过ref调用Children 组件的childInput方法,获取子组件input的焦点this.parentRef.current.childInput();}render() {return (<Children ref={ this.parentRef } />);}
}
3. ref属性不可在函数组件上使用,因为函数组件没有实例。
3. useRef()
useRef 是hooks家族中的一员,他在react hook中的作用,像是一个变量,类似如this,它可以存放任何的东西。createRef每次都会创建一个新的实例,而useRef每次都会返回相同的引用,返回的ref对象在组件的整个生命周期内保持不变。 因为特性的不同,createRef和useRef也有很大的不同
function TestTemp() {const [renderIndex, setRenderIndex] = useState(1);const refFromUseRef = useRef();const refFromCreateRef = createRef();if (!refFromUseRef.current) {refFromUseRef.current = renderIndex;}if (!refFromCreateRef.current) {refFromCreateRef.current = renderIndex;}return (<div>index: {renderIndex}<br />在refFromUseRef:{refFromUseRef.current}<br />在refFromCreateRef:{refFromCreateRef.current}<br /><button onClick={() => setRenderIndex(prev => prev + 1)}>增加</button></div>);
}
执行上面的代码,会发现index和refFromCreateRef是一直在随着点击增加的,而refFromUseRef则是保持不变。 就算组件重新渲染,由于refFromUseRef的值一直存在,类似于this,无法重新赋值,因此结果不会变。当ref对象内容发生变化时,useRef并不会通知你,更改current属性也不会导致重新渲染,因为它一直时一个引用。
何时适合用useRef
那么为什么要有useRef这个API呢,我们来看一下下面的例子
function TestTemp() {const [count, setCount] = useState(0);function handleAlertclick() {setTimeout(() => {alert("count:" + count);}, 2000);}return (<div><p>当前count: {count} </p><button onClick={() => setCount(count + 1)}>增加</button><button onClick={handleAlertclick}> 弹窗</button></div>)
}
按照如下步骤执行操作: 1. 点击两次增加按钮后,点击弹窗按钮 2. 在弹窗未展示之前,迅速再次点击两次增加按钮,等待弹窗出现
执行操作后发现,弹窗显示的结果是"count: 2",页面上的count值为4。弹窗显示的只是当时点击时的快照。
为什么弹窗中不是最新的count呢?
当我们更新状态时,React会重新渲染组件,每一次渲染都会拿到当前的count值,并重新渲染点击事件(handleAlertclick函数),因此每个函数里面都有自己的count。这样我们就理解了上面的例子中,弹窗中就是点击时的count值。
如何弹出时获取到实时的值呢?
function TestTemp() {const [count, setCount] = useState(0);const useRefCount = useRef(count);useEffect(() => {useRefCount.current = count;});function handleAlertclick() {setTimeout(() => {alert("useRefCount.current:" + useRefCount.current + "count:" + count);}, 2000);}return (<div><p>当前count: {count} </p><button onClick={() => setCount(count + 1)}>增加</button><button onClick={handleAlertclick}>弹窗</button></div>)
}
执行同上个例子中的操作,结果弹窗中显示"useRefCount.current: 4 count: 2"。使用 useRef 能获取到最新的值,但是 useState 却不能。 因为useRef每次都会返回同一个引用,因此在useEffect中修改时,弹窗中的也会被修改。
4. 过时API:string ref
class TestTemp extends React.Component {componentDidMount() {this.refs.myRef.focus();}render() {return <input ref="myRef" />;}
}
string ref是通过this.refs.myRef来访问DOM节点。但是现在不建议以这种方式创建ref属性,它已过时并可能在未来的版本被移除。React官方文档中有提出
如果你目前还在使用 this.refs.textInput 这种方式访问 refs ,我们建议用回调函数或 createRef API 的方式代替。
三、Refs转发
通常我们不需要在父组件中引用子组件中的DOM节点,但是在一些特殊情况下,避免不了这种需求,比如一些可重用的组件。因此我们可以使用ref转发,ref转发使组件可以像暴露自己的ref一样暴露子组件的ref。
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。 如果你使用16.3或者更高版本的React可以使用React.forwardRef来获取传递的ref,并且传递给需要引用的DOM元素
// React.forwardRef()接收ref作为第二个参数
// 子组件接收此ref并且传递给DOM元素
const Child = React.forwardRef((props, ref) => (<button ref={ref}>{props.children}</button>
));
// 父组件创建一个ref传递给子组件
// 通过myRef.current就可以引用button元素
const myRef = React.createRef();
<Child ref={myRef}>点击</Child>
如果你使用的是低版本的React,可以通过props传递ref来模拟React.forwardRef()函数
function Child(props) {return (<div><input ref={props.childRef} /></div>);
}
class Parent extends React.Component {constructor(props) {super(props);this.myRef = React.createRef();}componentDidMount() {this.myRef.current.focus();}render() {return (<Child childRef={this.myRef} />);}
}
注意:ref只在React.forwardRef()函数中作为第二参数存在,在常规的函数和Class组件是不接收ref参数的
四、Refs 传递原理
React中,HostComponent、ClassComponent、ForwardRef可以赋值ref属性。ForwardRef只是将ref作为第二个参数传递下去,没有别的特殊处理。 Ref属性在ref不同的生命周期会被执行 ( fuction类型 ) 或赋值 ( {current: any}对象类型 ) ref的生命周期与react的渲染一样,可以分为两个阶段
render阶段:
为含有ref属性的component对应fiber添加Ref effectTag, fiber类型为HostComponent、ClassComponent、ScopeComponent
commit阶段:
当给HTML元素添加ref属性时,ref回调接收了底层的DOM元素作为参数。在源码中有以下两个方法 1. commitAttachRef 挂载实例,当组件渲染完成后,在componentDidMount/componentDidUpdate后被执行。finishedWork为含有Ref effectTag的fiber 挂载时将DOM元素传入ref的回调函数
2. commitDetachRef 移除实例,在组件或元素被销毁前执行,在componentWillUnmount之前清理引用。 卸载时传入null,将当前的DOM元素赋值为null