接下来我们要学习最后一个属性ref
了,官网是这样描述的:
React 支持一个特殊的、可以附加到任何组件上的 ref 属性。此属性可以是一个由 React.createRef() 函数创建的对象、或者一个回调函数、或者一个字符串(遗留 API)。当 ref 属性是一个回调函数时,此函数会(根据元素的类型)接收底层 DOM 元素或 class 实例作为其参数。这能够让你直接访问 DOM 元素或组件实例。
谨慎使用 ref。如果你发现自己经常使用 ref 来在应用中“实现想要的功能”,你可以考虑去了解一下自上而下的数据流。
案例分析
我们写一个获取输入框内容的案例,来说明ref的使用。
<!-- 准备好员工“容器” --> <div id="app"></div> <!-- 引入ReactJS核心库 --> <script type="text/javascript" src="../JS/react.development.js"></script> <!-- 引入React-DOM核心库,用于操作DOM --> <script type="text/javascript" src="../JS/react-dom.development.js"></script> <!-- 引入Babel,用于编译jsx为js --> <script type="text/javascript" src="../JS/babel.min.js"></script> <!-- 此处类型为babel --> <script type="text/babel"> class Demo extends React.Component { render () { return ( <div> <input type="text" placeholder="点击按钮获取值" /> <button >click获取值</button> <input type="text" placeholder="失去焦点获取值" /> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app')) </script>
我们要对以上组件实现2个功能:
- 点击按钮获取第一个输入框的值
- 第二个输入框失去焦点时获取其值
传统方法:通过属性id
获取元素节点,拿到其值
class Demo extends React.Component { // 获取第一个输入框的值 getInput1 = () => { const input1 = document.getElementById('input1') console.log(input1.value) } // 获取第二个输入框的值 getInput2 = () => { const input2 = document.getElementById('input2') console.log(input2.value) } render () { return ( <div> <input id="input1" type="text" placeholder="点击按钮获取值" /> <button onClick={this.getInput1}>click获取值</button> <input id="input2" onBlur={this.getInput2} type="text" placeholder="失去焦点获取值" /> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,我们是通过传统的获取元素id
来拿到值,功能可以实现,但是在react
中我们就要使用react
的方法。
使用字符串的ref获取输入框的值:
class Demo extends React.Component { // 获取第一个输入框的值 getInput1 = () => { const input1 = this.refs.input1 console.log(input1.value) } // 获取第二个输入框的值 getInput2 = () => { const input2 = this.refs.input2 console.log(input2.value) } render () { return ( <div> <input ref="input1" type="text" placeholder="点击按钮获取值" /> <button onClick={this.getInput1}>click获取值</button> <input ref="input2" onBlur={this.getInput2} type="text" placeholder="失去焦点获取值" /> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,我们是通过ref
标识元素节点来获取输入框的值,功能可以实现,且代码比传统方法简洁。
注意:在React
中,字符串形式的ref
已经被废弃,不再推荐使用。字符串形式的ref
是一种在React
早期版本中使用的方式
使用回调函数的Ref
class Demo extends React.Component { // 获取第一个输入框的值 getInput1 = () => { const {input1} = this console.log(input1.value) } // 获取第二个输入框的值 getInput2 = () => { const {input2} = this console.log(input2.value) } render () { return ( <div> <input ref={c => this.input1 = c} type="text" placeholder="点击按钮获取值" /> <button onClick={this.getInput1}>click获取值</button> <input ref={c => this.input2 = c} onBlur={this.getInput2} type="text" placeholder="失去焦点获取值" /> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,我们使用的是箭头函数作为回调函数赋值给ref
,箭头函数本身没有this
,这里的this
其实是组件实例本身,我们这里回调函数获取到的参数其实就是ref
标识的这个节点元素,我们将当前节点赋值给这个组件实例。功能同样可以实现。
扩展内联回调Ref和class绑定的回调ref
关于回调 refs 的说明
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
以上是react
官方对内联函数的说明。
我们以代码来重现问题:
class Demo extends React.Component { // 获取第一个输入框的值 getInput1 = () => { const {input1} = this console.log(input1.value) } // 初始化状态 state = {isHot:true} // 切换天气 changeWeather = () => { const {isHot} = this.state this.setState({isHot:!isHot}) } render () { const {isHot} = this.state return ( <div> <h1>今天天气很{isHot?'炎热':'凉爽'}</h1> <input ref={(c)=>{this.input1 = c; console.log('@',c);}} type="text" /> <button onClick={this.getInput1}>click获取值</button> <button onClick={this.changeWeather}>修改状态</button> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,我们在内联函数中加了一个打印日志语句,初始化组件时我们的内联函数会执行一次,但是当我们切换天气更新组件状态时,内联函数会执行两次:
@ null @ <input type="text" />
这是为什么呢?
这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。
根据官方的解释,是因为我们更新组件状态时,react发现我们这个是一个内联函数,调用第一次的时候清空旧的实例,所以我们第一次打印的是null,清空以后设置了一个新的实例,所以我们第二次拿到的就是新的实例。
使用class
绑定的回调函数给ref
class Demo extends React.Component { // 获取第一个输入框的值 getInput1 = () => { const {input1} = this console.log(input1.value) } // 初始化状态 state = {isHot:true} // 切换天气 changeWeather = () => { const {isHot} = this.state this.setState({isHot:!isHot}) } // 绑定ref的回调函数 inputRef = (c) => { this.input1 = c; console.log('@',c) } render () { const {isHot} = this.state return ( <div> <h1>今天天气很{isHot?'炎热':'凉爽'}</h1> {/*<input ref={(c)=>{this.input1 = c; console.log('@',c);}} type="text" />*/} <input ref={this.inputRef} type="text"/> <button onClick={this.getInput1}>click获取值</button> <button onClick={this.changeWeather}>修改状态</button> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,我们使用class
绑定的函数给ref
进行回调,当我们进行状态更新的时候,其打印语句只执行一次。
react推荐的createRef
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
createRef 是 React 16.3 引入的一个 API,用于创建一个 ref 对象,可以将其赋值给组件的 ref 属性,从而可以在组件外部访问组件内部的 DOM 节点或组件实例。
class Demo extends React.Component { myRef1 = React.createRef() myRef2 = React.createRef() getData = () => { const val1 = this.myRef1.current.value console.log(val1) } blurData = () => { const val2 = this.myRef2.current.value console.log(val2) } render () { return ( <div> <input ref={this.myRef1} type="text"/> <button onClick={this.getData}>click</button> <input ref={this.myRef2} onBlur={this.blurData} type="text"/> </div> ) } } // 2、将虚拟DOM渲染到页面,标签必须闭合 ReactDOM.render(<Demo />,document.getElementById('app'))
以上代码,ref
赋值的是由createRef
创建出来的ref对象。用于接收底层 DOM
元素作为其 current
属性。
总结
在React中,ref 是一个特殊的属性,用于引用组件内部的 DOM 节点或组件实例。ref 属性可以是一个字符串,也可以是一个回调函数,还可以是一个 React.createRef() 创建的 ref 对象。
使用字符串作为 ref 属性的值已经被废弃,不推荐使用。推荐的做法是使用回调函数或 React.createRef()。
注意:在函数组件中不能使用ref属性,因为它们没有实例。