前言
本文为意译,翻译过程中掺杂本人的理解,如有误导,请放弃继续阅读。
原文地址:Refs and the DOM
正文
Refs提供了一种访问在render方法里面创建的React element或者原生DOM节点的方法。
在典型的React数据流中(自上而下的数据流),props是父组件与子组件打交道的唯一途径。为了与子组件交互,你需要给子组件传递一个新的props,促使它重新渲染。然而,有不少的场景需要我们在这种props主导型的数据流之外去命令式地去修改子组件里面的东西。被修改的子组件有可能是一个React component的实例,也有可能是一个原生DOM元素。对于这两种情况,React都提供了一个“安全舱口”去访问它们。
什么时候用Refs呢?
以下几个业务场景是挺合适的:
- 手动管理聚焦(focus),文本选择或者视频,音频的回放。
- 命令式地触发动画。
- 与第三方的DOM类库进行整合。
如果能用声明式的方式去实现的,就不要用refs去实现。举个例子说,能通过传递一个isOpen
prop给Dialog
组件来实现弹窗的打开和关闭,就不用通过暴露“open”和“close”方法来实现弹窗的打开和关闭(在结合redux数据流的背景下,我不太认同这句话所表达的观点)。
不要滥用Refs
在实际开发的过程中,如果实现上遇到困难了,你的惯性思维可能是,先使用refs实现了它(这个功能),管它三七二十一呢。在这种情况下,你不妨先让你的头脑冷静下来,以更缜密的思维去想想,能不能通过state来实现呢?如果能,state应该存放在组件树层级中的哪个层级呢?一般来说,公认为合适存放state的层级是顶级组件,也就是我们以前说的“container component”。查看提升你的state 看看该怎么做。
注意,接下来的例子已经被更新过了。更新过后的例子使用了React.createRef()这个API。这个API在React 16.3中就引入了。假如你还在使用较早的React版本,那么我们推荐你使用callback refs来代替。
Object Refs
创建Refs
使用React.createRef()来创建refs,并通过ref属性来attached to React element。一般的做法是,在组件的constructor里面,将通过React.createRef()来创建的refs直接赋值给组件的一个实例属性。这样一来,当组件实例化的时候,你就可以在组件的其他地方使用这个引用。(也就是说,组件实例的某个属性是引用着通过React.createRef()来创建的Refs的,而这个refs又是通过ref属性附加在原生DOM元素或者子组件实例上的。故通过这个实例属性,我们是可以访问到原生DOM元素或者子组件实例的)
class MyComponent extends React.Component {constructor(props) {super(props);this.myRef = React.createRef();}render() {return <div ref={this.myRef} />;}
}
访问Refs
上面讲了如何创建refs,这一小节我们就来讲如何访问refs。其实上面已经讲到了,我们通过组件的实例属性来保存着refs的引用的。refs是一个对象,它有一个current属性。我们正是通过这个current属性来访问原生DOM元素或者子组件实例的。
const node = this.myRef.current;
current的属性值因不同的情况而异。这个不同的情况指的是ref属性所在的React component的类型。
- DOM component。当ref属性是负载在DOM component上的话,通过React.createRef()来创建的Refs对象的current属性的值将会是原生的DOM元素。
- custom component。custom component又可以分为class component 和function component。注意,因为function component是没有实例的,所以,它是不能通过这种方式来使用ref属性的(这里,这种方式是指上面“创建refs”这一小节所说的方法。实际上function component也是可以消费ref属性的,这得使用后面提到的“ ref forwarding”技术)。所以这里,custom component指的是class component。当ref属性是挂载在custom component上的话,通过React.createRef()来创建的refs对象的current属性将会指向custom component的组件实例。
下面的例子将会演示这两者之间的不同。
1)把ref属性挂载在DOM component上
下面的代码使用了ref属性来保存DOM元素的引用:
class CustomTextInput extends React.Component {constructor(props) {super(props);// create a ref to store the textInput DOM elementthis.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 nodethis.textInput.current.focus();}render() {// tell React that we want to associate the <input> ref// with the `textInput` that we created in the constructorreturn (<div><inputtype="text"ref={this.textInput} /><inputtype="button"value="Focus the text input"onClick={this.focusTextInput}/></div>);}
当ref属性所在的那个组件挂载到页面后,current属性将会被赋值为指向原生DOM元素的引用。当这个组件被卸载后,current属性又会被重置为null。ref属性值的更新发生在组件生命周期函数componentDidMount
和componentDidUpdate
之前。
2)把ref属性挂载在class component上
如果你想把上面的<CustomTextInput>
包裹在父组件中,并想模拟组件挂载后就自动获取焦点。那么,我们可以通过ref去访问那个<CustomTextInput>
的实例,通过这个实例的focusTextInput
方法手动地让对应的组件内部的input框获得焦点。
class AutoFocusTextInput extends React.Component {constructor(props) {super(props);this.textInput = React.createRef();}componentDidMount() {// 在这里this.textInput.current指向的是// CustomTextInput组件的实例this.textInput.current.focusTextInput();}render() {return (<CustomTextInput ref={this.textInput} />);}
}
注意,<CustomTextInput>
组件是class component时,这种写法才会有用。
class CustomTextInput extends React.Component {// ...
}
3)Refs与function component的关联
注意,第三点,我们已经不使用“把ref属性挂载在xxx组件上”这个说法了。因为把ref属性直接挂载function component是没有什么用的。这里用“关联”一词,只不过在表达,refs还是可以跟function component结合起来使用的。 再次强调,应为function component没有实例,所以不要直接在function component上挂载ref属性:
function MyFunctionComponent() {return <input />;
}
class Parent extends React.Component {constructor(props) {super(props);this.textInput = React.createRef();}render() {// This will *not* work!return (<MyFunctionComponent ref={this.textInput} />);}
}
如果你想在一个组件上直接挂载ref属性,那么你需要将这个组件转化为class component。这种转化,就像你如果需要使用生命周期函数或者state,你也会将组件转化为class component一样。
然而,正如我们上面提到的,ref属性还是可以跟function component结合使用的。如何结合法呢?那就是在function component的实现代码体里面使用。值得注意的是,即使在function component里面去使用,ref属还是要挂载在DOM component或者class component上:
function CustomTextInput(props) {// textInput must be declared here so the ref can refer to itlet textInput = React.createRef();function handleClick() {textInput.current.focus();}return (<div><inputtype="text"ref={textInput} /><inputtype="button"value="Focus the text input"onClick={handleClick}/></div>);
}
将DOM Refs暴露给父组件
在很少的情况下,你可能想直接从父组件来访问子组件里面的DOM元素。而一般情况下,我们是不推荐大家这么做的。因为这么做会打破组件封装的完整性。但是呢,偶尔这么做还是挺管用的。比如想手动让输入框获取焦点或者测量子组件中DOM元素的位置和尺寸大小。
如果你正在使用React 16.3或者以上,我们推荐你使用ref forwarding来满足你的需要。Ref forwarding技术让子组件自己选择是否要把ref引用暴露给父组件(Ref forwarding lets components opt into exposing any child component’s ref as their own)。你可以查阅一下ref forwarding文档。这里的例子将会给你演示如何地将子组件中DOM元素的ref引用暴露给父组件的。
如果你再用React 16.2或者以下,或者需要一个比ref fowarding更加灵活的方案,你可以使用这个方案。通过一个不同与ref的prop名,显式地将ref引用传递下去。
我们建议尽可能少地去暴露DOM元素给外界。但是,它确实可以是一个很有用的(访问原生DOM)“安全舱口”。注意,这种访问原生DOM的方案需要你往子组件中添加一些代码。假如你对子组件的实现没有控制权(即往里面插入一些代码),那么这个时候你只剩下最后的选择了-使用findDOMNode()。原则上,findDOMNode()已经不被鼓励使用了。在 StrictMode下,这个API已经被废弃了。
Callback Refs
除了上面提到的方法外,React也支持别的方式去设置refs,其中一个就叫“callback refs”。“callback refs”能够在refs赋新值和重置的时候给你更小粒度的控制权。
相比于使用createRef()来创建refs并将它传递给ref属性,“callback refs”传递给ref属性的是一个函数,准备来说是一个callback函数。在这个callback函数里面,你可以通过参数获得一个访问React component实例或者原生的DOM元素的引用。一般的做法,使用一个组件的实例属性来保存这个引用,方便到处使用:
class CustomTextInput extends React.Component {constructor(props) {super(props);this.textInput = null;this.setTextInputRef = element => {this.textInput = element;};this.focusTextInput = () => {// Focus the text input using the raw DOM APIif (this.textInput) this.textInput.focus();};}componentDidMount() {// autofocus the input on mountthis.focusTextInput();}render() {// Use the `ref` callback to store a reference to the text input DOM// element in an instance field (for example, this.textInput).return (<div><inputtype="text"ref={this.setTextInputRef}/><inputtype="button"value="Focus the text input"onClick={this.focusTextInput}/></div>);}
}
当组件挂载到页面后,React将会给这个callback函数传入个一个组件实例或者原生DOM的引用;当组件卸载后,React再次给callback函数传入null(这里说的“给callback函数传入”代表着一次callback的调用)。React保证refs的更新会在componentDidMount和componentDidUpdate调用之前发生。
正如由React.createRef()创建出来的对象类型refs一样,你可以将函数类型的refs一路传递下去。
function CustomTextInput(props) {return (<div><input ref={props.inputRef} /></div>);
}
class Parent extends React.Component {render() {return (<CustomTextInputinputRef={el => this.inputElement = el}/>);}
}
在上面的例子中,父组件<Parent>
将函数类型的refs以一个叫inputRef的prop传递给<CustomTextInput>
组件。然后<CustomTextInput>
组件用真正的ref属性传递给<input>
组件。从结果来看,<Parent>
的实例属性inputElement引用的正是我们想要访问的,<CustomTextInput>
组件里面的<input>
DOM元素。
Legacy API: String Refs
如果你之前有使用过React,那么你应该知道ref属性的值可以是一个字符串,比如:“textInput”。然后你可以通过this.refs.textInput来访问原生的DOM元素。我们强烈建议你不要再使用它了。因为它存在某些问题,同时它已经被遗弃了。在未来的某个版本中,很有可能会把它的实现从代码中移除掉。
注意,如果你现在正在使用this.refs.textInput来访问refs,那么我们推荐你使用callback pattern或者createRef API来代替。
使用callback refs的注意点
如果你把一个inline function直接赋值给ref属性的话,那么这个inline function将会被调用两次。第一次是以null来调用的。第二次才是以真实的DOM元素去调用。这是因为,每一次render方法被调用的时候,inline function都会创建一个新的函数实例给ref属性。所以,React需要先移除旧的ref callback,再来设置新的。为了避免这个问题,你可以通过把这个inline function绑定成class component的方法,使之成为引用,然后将这个引用赋值给ref属性。不过大多数情况下,inline function不会造成什么大问题的。
零基础入门
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
同时每个成长路线对应的板块都有配套的视频提供:
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享
视频配套资料&国内外网安书籍、文档&工具
当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享