什么是Refs
我们在日常写React代码的时候,一般情况是用不到Refs这个东西,因为我们并不直接操作底层DOM元素,而是在render函数里去编写我们的页面结构,由React来组织DOM元素的更新。
凡事总有例外,总会有一些很奇葩的时候我们需要直接去操作页面的真实DOM,这就要求我们有直接访问真实DOM的能力,而Refs就是为我们提供了这样的能力。
看这个名字也知道,Refs其实是提供了一个对真实DOM(组件)的引用,我们可以通过这个引用直接去操作DOM(组件)
为什么会用到这个
上面有提到,我们一般情况下是不需要用到这个东西,那具体什么时候才会用到呢? 看官方建议:
- Managing focus, text selection, or media playback.
- Triggering imperative animations.
- Integrating with third-party DOM libraries.
简单的来说就是处理DOM元素的focus,文本的选择或者媒体的播放等,以及处罚强制动画或者同第三方DOM库集成的时候。
也就是React无法控制局面的时候,就需要直接操作Refs了。
怎么用
React V16版本之前,
我们一般都是通过一个回调函数的方式,把当前组件的DOM绑定到一个实例变量上,像下面这样:
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
}
componentDidMount() {
this.textInput.focusTextInput();
}
render() {
return (
<CustomTextInput ref={ele => { this.textInput = ele}} />
);
}
}
在上面的代码中,我们先声明一个值为null的textInput变量,然后在ref中以回调的方式将组件DOM赋值给textInput。然后就可以通过 this.textInput.focus()这样的性质来直接调用CustomTextInput这个组件的实例方法。
但是这个方式有以下两个不太好:
- 每次组件重新渲染的时候,行内函数都会执行两次,第一次的ele的值为空,第二次才为真正的DOM对象。
因为在每次渲染中React都会创建一个新的函数实例。因此,React 需要清理旧的 ref 并且设置新的。
通过将 ref 的回调函数定义成类的绑定函数的方式可以避免上述问题, - 如果我们想要将一个子组件的ref传递给父组件,可能会有点麻烦,虽然通过一个特殊的prop属性可以做到,但是感觉有点不太正规。。。
React V16 版本后
React V16版本新增一个API:React.createRef(); 通过这个API,我们可以先创建一个ref变量,然后再将这个变量赋值给组件声明中ref属性就好了。
具体看代码:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
在上面的代码中,我们先通过 React.createRef();创建一个ref,并赋值给组件属性textInput(this.textInput),然后在render函数中通过ref={this.textInput}的方式将ref和input这个真实DOM联系起来, 这样就可以通过 this.textInput.current.focus();的方式来直接操作input元素的方法。
不同之处
在V16版本前,我们可以直接通过变量访问元素的方法,在V16后,我们需要通过 this.textInput.current,即真实的DOM是通过current属性来引用的。
如果通过 createRef()这个API赋值给组件的ref,那么引用的就是组件实例;如果是DOM元素,那引用的自然的就是DOM元素了。。
传递Refs
前面我们说到,在V16版本之前,我们想要父组件拿到子组件的ref,需要通过一些特殊的方法,V16版本之后,React提供了一种原生的方式来完成这种操作。
这就涉及到React新增的另一个API: React.forwardRef(), 通过接受一个函数,来传递refs,具体如下:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
- 首先我们通过React.createRef();创建一个ref变量,然后在FancyButton属性中通过 ref={ref}的方式把这个ref和组件关联起来。
- 目前为止,如果FancyButton 是一个通过class或者函数声明的组件,那么就到此为止,我们可以说 ref变量的current属性持有对 FancyButton组件实例的引用。
- 不幸的是,FancyButton经过了 React.forwardRef的处理, 这个API接受两个参数,第二个参数就是ref,然后通过 <button ref={ref}>把ref绑定到button元素上,这样ref.current的引用就是button元素这个DOM对象了。。。
上面的有点绕,简单来说,就是我们创建一个引用,本来是给外面的FancyButton组件的,但是因为React.forwardRef的处理,这个引用被传递给了内部的button元素。这样ref.current的引用由本来的FancyButton实例传递到了button元素本身。
在HOC组件中的应用
HOC(higher-order components)高阶组件,简单的说,就是通过组件包裹的方式来提到代码复用,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
以下是一个生成高阶组件的函数:
function logProps(WrappedComponent) {
class LogProps extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
return LogProps;
}
logProps是函数,接受一个组件参数,返回一个包裹参数组件的logProps组件。
下面是用法:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);
我们先声明一个FancyButton的组件,然后将其作为参数传入logProps函数,最后得到的其实是一个LogProps组件。
接下来我们使用refs:
import FancyButton from './FancyButton';
const ref = React.createRef();
// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
我们通过文件引入FancyButton(其实引入的是LogProps组件)然后createRef并指向FancyButton。 本意是希望引入真正的FancyButton组件,实际上引用的是 外层包裹组件LogProps组件。
我们可以通过以下改造来完善代码:
function logProps(Component) {
class LogProps extends React.Component {
render() {
const {forwardedRef, ...rest} = this.props;
// Assign the custom prop "forwardedRef" as a ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// Note the second param "ref" provided by React.forwardRef.
// We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
// And it can then be attached to the Component.
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
如面的代码所示,我们修改了高阶组件logProps函数的实现方式,在内部组件LogProps的render方法中,给被包裹组件(作为参数传入的组件)添加了来自props的ref。
最终返回的也是一个React.forwardRef处理过的组件,这个组件将ref传递到内部的props中去。
这样,但我们通过logProps(FancyButton)函数调用的时候,其实返回的是一个经过React.forwardRef处理的组件, 当通过
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
去添加ref的时候, 这个ref其实直接添加到了内部的LogProps组件的forwardedRef属性上,然后在LogProps组件内部,又通过props属性的方式被赋值了 被包裹组件(作为参数的组件,也就是FancyButton组件)。这个传递其实经过了三次。。。。
总的来说,高阶组件的ref其实是通过React.forwardRef技术将ref传递到包裹组件logProps上,然后有通过属性传递 传递到真正的FancyButton组件上,两次传递才完成。。。。