react 使用 refs
前面两篇博客介绍了 react 的两大模块 state 和 props。然后呢,今天来介绍最后一个 ref。学习过 vue 项目的宝子们应该对于 ref 不是很陌生,那在 react 里面 ref 的作用和 vue 是差不多的。
案例
同样呢,我们还是通过一个案例的方式介绍 react 的 ref。
现在我们想做这样一件事情,就是编写一个页面,有一个输入框,可以向里面输入任何东西,再有一个按钮,点击按钮的时候,把输入框里面输入的内容弹出来;然后再在按钮后面写一个输入框,也是可以随便输入内容,输入完成,当这个输入框失去焦点的时候,弹出第二个输入框输入的内容。效果如下面:
好的,下面我就就实现一下。
字符串形式的 ref
上面案例的要求明白了,然后我们就可以简单的先写一个代码,咱先不用 ref,咱先使用 js 原始的方式实现入框的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>23.字符串形式的ref</title>
</head>
<body>
<!-- 准备好一个容器 -->
<div id="app"></div>
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom 用于支持 react 操作 dom -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入 label,用于将 jsx 转化为 js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<!-- 引入 prop-types,用于对于组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<!-- 此处必须写 text/babel -->
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
// 展示左侧数据
showData = () => {
let input1 = document.getElementById("input1")
alert(input1.value)
}
// 失去焦点展示数据
showData2 = () => {
let input2 = document.getElementById("input2")
alert(input2.value)
}
render() {
return (
<div>
<input id="input1" type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button>
<input id="input2" onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
</body>
</html>
上面的代码很简单,就是给输入框设置了一个 id ,然后使用 js 代码去获得节点数据,然后再去拿节点的 value 值弹窗展示,点击事件之前博客已经说过了,所以说不详细写了,就这样。保存上面的代码,然后刷新页面看效果。
OK,效果是可以实现的,我们接下来换 ref 的方式,使用 ref 需要怎么修改呢?很简单,我们首先把
id 改成 ref,注意哈,不是 refs ,而是 ref。
render() {
return (
<div>
<input ref="input1" type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
OK,我们在按钮点击事件里面打印一下 this,首先明白一点,这个 this 是谁? 对的,这个 this 是 Demo 的实例。我们看一下 this 是啥!
// 展示左侧数据
showData = () => {
console.log(this)
}
保存刷新一下,直接点击按钮:
诶,我们看到打印出来 this 了。 this 里面有许多的东西,像是 props 和 state 是啥不说了,之前博客都说的很详细了,然后我们重点关注 refs,这个 refs 就是我们的两个 input 输入框,因为我们给 两个 input 输入框设置了 ref 分别是 input1 和 input2,所以说,实例里面就把两个 ref 添加到了 refs 对象里面。
那我们打印一下 refs 里面的 input1 看一下是啥。
// 展示左侧数据
showData = () => {
console.log(this.refs.input1)
}
看一下运行的结果。
输出了 第一个输入框节点。OK,我们在 第一个输入框的代码里面 添加了 ref 属性,请问,代码里面第一个输入框是虚拟 Dom 还是真实 Dom?很显然是虚拟Dom,那在点击事件里面打印的 input1 是虚拟 Dom 还是真实 Dom?对,真实 Dom。
既然是真实 Dom,那我们点击按钮就能够拿到 input1 的真实 Dom,所以说获取 value 是不是就和 js 原生写法一样了?那我们修改代码,再次实现案例的要求。
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
// 展示左侧数据
showData = () => {
// 因为 this.refs 是对象,我们可以解构使用
let { input1 } = this.refs
alert(input1.value)
}
// 失去焦点展示数据
showData2 = () => {
let { input2 } = this.refs
alert(input2.value)
}
render() {
return (
<div>
<input ref="input1" type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
保存刷新,查看一下效果。
OK,这样我们就成功的使用了 refs。当然我们可以看到,我们给 input 输入框设置 ref 属性的时候是用的字符串类型的,所以说这部分是说的 字符串类型的 ref 使用。这部分就到这里。
【本部分相关代码资料】:我是𝒆𝒅. 的 gitee
回调形式的 refs
上边一部分我们说了 字符串形式的 ref 使用,很简单是吧?其实 ref 的使用啊,有很多中方式,其实到写博客这个时间点是三种啦,这三种里面,字符串形式使用 ref 是最简单的,这一部分我们学习一个相对来说稍难一点的方式 —— 回调形式的 refs。
有宝子们会问了,为啥有简单的还要学习难的呢?很简单的道理,因为使用 字符串形式的 ref 这种方式,官方不建议我们使用,甚至在后期的 react 升级版本之后会弃用。看下面官方说明:
有宝子可能就会有意见了,为啥快弃用的还要说那么多!因为他只是说会弃用啊!但是现在还没有,我用的是 16 版本的 react ,现在还是没有弃用的,以后开发项目,我们会遇到各种各样的人写各种各样的代码,你可以管住你自己,但是你不能管住别人呀!所以说你不敢保证别人不用,所以得学一下。
接下来说一下 refs 使用的另一种方法:回调形式的 refs
回调形式的 refs 比 字符串形式的 refs 稍微复杂一点,但是也不是很难,修改的地方很简单,之前我们给 input 框设置 ref 的是后是这样设置的:
<input ref="input1" type="text" placeholder="请输入文字"></input>
如果是 使用回调函数 方式怎么设置呢?简单,既然是回调函数方式,那我先写一个回调函数这个没问题吧?
<input ref={() => {}} type="text" placeholder="请输入文字"></input>
其中 ref={ () => { } } 最外层的 { } 是告诉 jsx 我这个里面包裹的是 js 表达式,是一个回调函数。
然后这个回调有没有返回参数呢?我们试一下就可以啦呀!修改一下代码,在回调函数里面输出一下参数:
<input ref={(val) => {console.log('---> ',val)}} type="text" placeholder="请输入文字"></input>
首先明白一点儿,就是我们刷新页面的时候,这个在 { } 里面包裹的回调函数会不会执行?肯定会被执行对吧?之前的博客在点击事件的时候说了,肯定会执行,所以说我们直接刷新页面就会被打印对吧?好,刷新页面:
OK,控制台直接打印,打印出了啥?input,这个 input 是啥?就是他本身的 dom 节点啊!OK,所以说我们回调函数返回的参数,就是添加 ref 的 dom 节点,那你看下面的代码:
<input ref={(currentNode) => {this.input1 = currentNode}} type="text" placeholder="请输入文字"></input>
这段代码可以理解什么意思吗?就是 我获取到了 当前 ref 的 dom 节点,然后赋值给了 this.input1,请问 this 是谁? input1 是谁?是的,this 就是当前实例,input1 就是把当前节点的dom 赋值给了实例的 input1。我们看一下现在的实例,也就是 this。
OK,明白了吧?我们把 input1 的dom节点,赋值给了实例的 input1。所以说,我们点击按钮的方法里面需要怎么修改?
// 展示左侧数据
showData = () => {
let { input1 } = this
alert(input1.value)
}
input1 在哪里啦?在 Demo 实例上,所以说我们取值不需要再 this.refs,直接 this 就可以了吧!
好,同样我们修改一下 input2。
<!-- 此处必须写 text/babel -->
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
// 展示左侧数据
showData = () => {
let { input1 } = this
alert(input1.value)
}
// 失去焦点展示数据
showData2 = () => {
let { input2 } = this
alert(input2.value)
}
render() {
return (
<div>
<input ref={(currentNode) => {this.input1 = currentNode}} type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button>
<input ref={(currentNode) => {this.input2 = currentNode}} onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
保存,查看一下效果:
OK,效果完全一样。
我们在看一个地方:
<input ref={(currentNode) => {this.input1 = currentNode}} type="text" placeholder="请输入文字"></input>
这个代码的回调是使用箭头函数写的,所以箭头函数没有自己的this,如果要用this,他只能自动往外层找,外层是谁?对的,this 就是 Demo 实例。
这个地方可不可以简写啊?参数只有一个可以省去括号,回调语句只有一句,可以省略花括号。
<input ref={currentNode => this.input1 = currentNode} type="text" placeholder="请输入文字"></input>
所以简写上边这样也是可以的哈。
好了这部分的内容就结束了,然后使用回调形式的ref 是在项目中最常见的也是建议使用的。
【本部分相关代码资料】:我是𝒆𝒅. 的 gitee
回调函数的 ref 调用次数问题
上部分我们简单说了一下 回调函数ref 的使用,这部分我们稍微说一下残存的一个问题,就是他回调调用了几次?
为啥想说这个问题,因为官网给了一句话。
啥意思呢,就是说如果我们使用的是 回调函数形式的 ref,那么在页面更新的时候,回调两次,然后第一次传入的是空。
比如之前代码我们修改一下 input1 的代码,我们除了赋值,在打印一下返回的回调参数:
<input ref={currentNode => {this.input1 = currentNode, console.log('调用了----> ', currentNode) }} type="text" placeholder="请输入文字"></input>
刷新页面:
看到没,刷新页面直接走了回调并且可以及时打印出回调的 dom 节点数据。
官网啥意思呢,就是页面更新了,会被执行两次,简单点说就是控制台会打印两次,但是,第一次拿不到 dom 节点,是 null,第二次才会正确打印 dom。
要想看这个效果需要怎么做呢?麻烦,记得我们之前有篇博客学习 state,里面的案例是实现 动态显示今天上班不上班的案例吗?我们需要结合进来,在切换上班不上班的时候,页面就会更新对吧?这时候,这个 refs 的 bug 就会触发了。OK,我们把那个案例的代码整合进来!
<!-- 此处必须写 text/babel -->
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
state = { isWork: true }
// 展示左侧数据
showData = () => {
let { input1 } = this
alert(input1.value)
}
// 失去焦点展示数据
showData2 = () => {
let { input2 } = this
alert(input2.value)
}
// 改变是否上班
changeWork = () => {
let { isWork } = this.state
this.setState({ isWork: !isWork })
}
render() {
let { isWork } = this.state
return (
<div>
<span>今天老子 {isWork ? '上班' : '不上班'}</span> <br />
<input ref={(currentNode) => { this.input1 = currentNode; console.log('执行了---> ', currentNode) }} type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button> <br />
<button onClick={this.changeWork}>点击我切换是否上班</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
上面代码是关于 state 案例的,具体代码作用不细说,不懂的宝子可以看我之前的博客。保存,刷新页面,我们点击 “切换是否上下班” 按钮,促使页面更新,我们看一下打印情况。
我们看到,除了第一次刷新页面打印的,点击按钮之后还打印了两次,第一次确实为 null,第二次才是正常的。
这是为啥呢?
官方说的很清楚了!
稍微分析一下,我不知道我能不能说清楚。就是刷新页面的时候,切记是第一次哦!执行 render 函数渲染标签,他在 input 地方识别到你写了 ref,并且是个回调,OK,他给你把标签作为回调参数返回给你,你可以正常打印,这是第一次的哈。当你 state 改变之后,状态驱动页面,所以说页面更新,更新需要重新执行 render 吧?所以说 input 是不是要重新渲染呀?重新渲染的时候遇到问题了,他识别到你是用了 ref ,但是由于是使用的 回调函数形式,注意哈,这里的回调不是之前的回调了,成了一个新的,之前的在更新的时候释放掉了!所以说他不确定你之前回调的做了些什么传了些什么,所以他为了保证这个 input 被完美的清空,直接回了一个 null,然后紧接着他又调用了一次,才把当前的节点给你返回。这就是为啥出现上面效果的原因。
那出现这个问题,会有什么影响呢?
官方也给出解释了。没影响,无关紧要。
但是我们能不能解决一下这个问题呢?答案是可以的,官网也说了。
这句话是什么意思?看上去很难懂,但是很简单。
写一下就明白了,简单点说,就是把回调拿出去,不要写在标签里面了:
<!-- 此处必须写 text/babel -->
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
state = { isWork: true }
// 展示左侧数据
showData = () => {
let { input1 } = this
alert(input1.value)
}
// 失去焦点展示数据
showData2 = () => {
let { input2 } = this
alert(input2.value)
}
// 改变是否上班
changeWork = () => {
let { isWork } = this.state
this.setState({ isWork: !isWork })
}
// 保存节点
saveInput = (currentNode) => {
this.input1 = currentNode
console.log('---> ', currentNode)
}
render() {
let { isWork } = this.state
return (
<div>
<span>今天老子 {isWork ? '上班' : '不上班'}</span> <br />
{/* <input ref={(currentNode) => { this.input1 = currentNode; console.log('执行了---> ', currentNode) }} type="text" placeholder="请输入文字"></input>*/}
<input ref={this.saveInput} type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button> <br />
<button onClick={this.changeWork}>点击我切换是否上班</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
看代码,我们把第一个输入框的 ref 修改为 ref={this.saveInput} ,让他去调用 saveInput 方法,方法里面怎么写呢,就是之前的写法。
// 保存节点
saveInput = (currentNode) => {
this.input1 = currentNode
console.log('---> ', currentNode)
}
我们现在在打印下看看。
看到没,只打印了刷新页面的一次,后边的都没有再执行了。为啥呢,因为更新页面的时候,他会重新执行 render,之前打印是因为回调写的是内联写法,每次更新都是一个新的,要重新执行,但是我们把他拆出来,把回调写在了 实例自身,他在渲染的时候就知道,奥,这不是一个新的回调,我之前调用过了,所以说不会频繁执行回调。
记住一点哈,在实际开发过程中,我们一般使用 回调函数形式的 ref,而且不是非得把回调提出来,一般都写内联形式,他多次执行并不会影响我们整体效果。
好了,这部分就完工了,还有一小部分,关于 refs 相关的知识就彻底结束了。坚持一下!
【本部分相关代码资料】:我是𝒆𝒅. 的 gitee
createRef 方式
好了,还有最后一种,最新的,但是不常用,说一下哈,万一接手的项目人家就是这么用的,其实用法很简单。
首先我们要在实例上创建一个属性接收一个容器,这么接收呢?
myRef = React.createRef()
React.createRef() 有什么作用呢?
React.createRef() 调用后可以返回一个容器,该容器可以储存被 ref 标识的节点。
然后给 input 输入框设置 ref 的时候怎么设置呢?
<input ref={this.myRef} type="text" placeholder="请输入文字"></input>
那我们在点击按钮的时候打印一下这个 myRef 好不好?
showData = () => {
console.log(this.myRef)
}
我们看一下这个 myRef 是啥
诶,我们发现打印出来的 myRef 就是一个对象,对象有一个属性叫做 current,对应的值就是 input 的 dom 节点,所以说我们想获取 dom 的 value 值的时候是不是直接就可以获取到了?
// 展示左侧数据
showData = () => {
alert(this.myRef.current.value)
}
保存看一下
好,是没有问题的,那我们给 input2 也来一个,怎么设置呢,记住了哈!
该容器是专人专用的!也就是说,只能存一个,后放进去的会把前面的给顶掉。
我们创建了一个 myRef 容器,给 input1 使用了,如果我们在把 myRef 容器给 input2 使用,那么好了,input1 就被顶掉了,只有 input2 是管用的,看一下效果。
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button> <br />
<input ref={this.myRef} onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
我们给 input1 和 input2 都设置 ref={this.myRef} 然后打印一下 myRef 看看一下现在是啥
看到没,只打印了一个,是 input2 的 input1 的被顶掉了,这么解决呢?也很简单,那就是在继续创建一个仓库。
<!-- 此处必须写 text/babel -->
<script type="text/babel">
// 创建组件
class Demo extends React.Component {
/**
* React.createRef: 调用后可以返回一个容器,该容器可以储存被 ref 标识的节点。
* 该容器是专人专用的!也就是说,只能存一个,后放进去的会把前面的给顶掉
*/
myRef = React.createRef()
myRef2 = React.createRef()
// 展示左侧数据
showData = () => {
alert(this.myRef.current.value)
}
showData2 = () => {
alert(this.myRef2.current.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="请输入文字"></input>
<button onClick={this.showData}>点击我弹出左侧内容</button> <br />
<input ref={this.myRef2} onBlur={this.showData2} type="text" placeholder="右侧数据" />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Demo />, document.getElementById("app"))
</script>
这样就可以了。
好的,今天我们关于 ref 的知识基本上就结束了,看到这儿的都是赏脸的啦,啊哈哈哈哈,本来就是想当自己学习记录,但是能作为分享知识的途径也是好的。
今天就到这里了,拜拜!
【本部分相关代码资料】:我是𝒆𝒅. 的 gitee
再见!我要去干饭啦!