首页 > 应用开发 > IT综合 > React Ref 其实是这样的
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是, 在 某些情况下,你需要在典型数据流之外强制修改子组件/元素。
适合使用 refs 的情况:
在 React v16.3 之前,ref 通过字符串(string ref)或者回调函数(callback ref)的形式进行获取。
ref 通过字符获取:
// string refclass MyComponent extends React.Component {
componentDidMount() { this.refs.myRef.focus();
}
render() { return <input ref="myRef" />;
}
}
ref 通过回调函数获取:
// callback refclass MyComponent extends React.Component {
componentDidMount() { this.myRef.focus();
}
render() { return <input ref={(ele) => {
this.myRef = ele;
}} />;
}
}
在 v16.3 中,经 0017-new-create-ref 提案引入了新的 API:React.createRef。
ref 通过 React.createRef 获取:
// React.createRefclass MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef();
}
componentDidMount() { this.myRef.current.focus();
}
render() { return <input ref={this.myRef} />;
}
}
当然还有最近react大力推崇的 hooks:useRef
function MyComponent() { const myRef = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素
myRef.current.focus();
}; return (
<>
<input ref={myRef} type="text" />
<button onClick={onButtonClick}>聚焦</button>
</>
);
}
首先来具体说说 string ref,string ref 就已被诟病已久,React 官方文档中如此声明:"如果你目前还在使用 this.refs.textInput 这种方式访问 refs ,我们建议用回调函数或 createRef API 的方式代替。",为何如此糟糕?
最初由 React 作者之一的 dan abramov。发布于https://news.ycombinator.com/edit?id=12093234,(该网站需要梯子)。吐槽内容主要有以下几点:
/** string ref **/class Parent extends React.Component {
componentDidMount() { // 可获取到 this.refs.childRef
console.log(this.refs);
}
render() { const { children } = this.props; return React.cloneElement(children, { ref: 'childRef',
});
}
}class App extends React.Component {
componentDidMount() { // this.refs.child 无法获取到
console.log(this.refs);
}
render() { return ( <Parent>
<Child ref="child" />
</Parent>
);
}
}
class MyComponent extends Component {
renderRow = (index) => { // string ref 会挂载在 DataTable this 上
return <input ref={'input-' + index} />; // callback ref 会挂载在 MyComponent this 上
return <input ref={input => this['input-' + index] = input} />;
}
render() { return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
关于这点可以参考 React 源码 coerceRef 的实现:
在调和子节点 得 过程中,会对 string ref 进行处理,把 他 转换成一个方法,这个方法主要做的事情就是设置 instance.refs[stringRef] = element,相当于把他转换成了function ref
对于更新得过程中string ref是否变化需要对比 得 是 current.ref._stringRef,这里记录了上一次渲染得时候如果使用得是string ref他的值是什么
owner是在调用createElement的时候获取的,通过ReactCurrentOwner.current获取,这个值在更新一个组件前会被设置,比如更新ClassComponent的时候,调用render方法之前会设置,然后调用render的时候就可以获取对应的owner了。
React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发 前,React 会保证 refs 一定是最新的。
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
React.createRef 的优点:
React.createRef 的缺点:
ref 的值根据节点的类型而有所不同:
这第四种使用 ref 的方法又有何不同呢?
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。 返回的 ref 对象在组件的整个生命周期内保持不变 。并且 useRef 可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
正是由于这些特性,useRef 和 createRef 出现了很大差异。
可以运行下以下代码:
import React, { useState, useRef, useEffect } from "react";export default function App() { const [count, setCount] = useState(0); const latestCount = useRef(count);
useEffect(() => {
latestCount.current = count;
}); function handleAlertclick() {
setTimeout(() => {
alert("latestCount.current:" + latestCount.current + '.. count: ' + count);
}, 2000);
}
return ( <div>
<p>当前count: {count} </p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={handleAlertclick}> 提示 </button>
</div>
)
}
然后按照下面步骤进行操作:
然后会你会得到一个有趣的答案:alert 弹窗会提示: latestCount.current:7.. count: 5。使用 useRef 能获取到最新的值,但是 useState 却不能。
具体原因可以参考 react 作者之一 dan 的个人博客。或者查看 React 函数式组件和类组件的区别,不是只有state和性能!
那么 useRef 真有那么很好用吗?并不是的。还有由于它上面的那个特性,问题还是不少的。
你可以尝试跑一下下面这段代码,或者 点击这里查看
import React, { useRef, createRef, useState } from "react";import ReactDOM from "react-dom";function App() { 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 className="App">
Current render index: {renderIndex} <br />
在refFromUseRef.current中记住的第一个渲染索引:
{refFromUseRef.current} <br />
在refFromCreateRef.current中未能成功记住第一个渲染索引:
{refFromCreateRef.current} <br />
<button onClick={() => setRenderIndex(prev => prev + 1)}>
数值 + 1 </button>
</div>
);
}const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
上面的案例中无论如何点击按钮 refFromUseRef.current 将始终为 1,而 renderIndex 和 refFromCreateRef.current 会伴随点击事件改变; 意想不到吧?
因为:当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 callback ref 来实现。
总结下:
由于上面的一些问题,起初我也是并不想把 useRef 作为操作 ref 的方法来讲的。
在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。
如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。
什么是 ref 转发?
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton">
{props.children} </button>));// 你可以直接获取 DOM button 的 ref:const ref = React.createRef();<FancyButton ref={ref}>Click me!</FancyButton>;
如果在低版本中如何转发?
如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用 ref 作为特殊名字的 prop 直接传递。
比如下面这样:
function CustomTextInput(props) { return (
<div>
<input ref={props.inputRef} />
</div>
);
}class Parent extends React.Component { constructor(props) { super(props); this.inputElement = React.createRef();
}
render() { return (
<CustomTextInput inputRef={this.inputElement} />
);
}
}
以下是对上述 示例 发生情况的逐步解释:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69923331/viewspace-2729645/ ,如需转载,请注明出处,否则将追究法律责任。