

闲庭信步聊前端 - 原来你是这样的Refs
source link: https://zhuanlan.zhihu.com/p/340564065
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

闲庭信步聊前端 - 原来你是这样的Refs

一、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 (
<Child
myRef={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


Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK