(15)React 进阶——⑤ React 中 ref 的使用 | React 基础理论实操

转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥"前端一万小时"两大明星专栏------"从零基础到轻松就业"、"前端面试刷题",已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

javascript 复制代码
 涉及面试题:
 1. refs 有什么用?
 2. 如何创建 refs?
 3. 什么是 forward refs?
 4. callback refs 和 findDOMNode() 哪一个是首选选项?
 5. 为什么 String Refs 被弃用?
 6. 什么是受控组件?
 7. 什么是非受控组件?
 8. 如何在页面加载时聚焦一个输入元素?
 9. 在 React 中如何以编程方式触发点击事件?
10. 你什么时候需要使用 refs?

编号:[react_15]

1 e.target

ref 是 reference 的简写,其含义为"引用"。

❗️在 React 中,我们可以使用 ref 来获取 DOM 元素。一般情况下,我们尽量不要去使用 ref,但有时我们会接触到一些极其复杂的业务(比如"动画"),其不可避免的还是会用到页面上的一些 DOM 标签。

紧接之前的代码,打开 TodoList.js 文件:

jsx 复制代码
import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {  
      inputValue: "", 
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() {  
    
    return(
      <Fragment>
        <div>
          <label htmlFor="insertArea">请输入要进行的事项:</label>    
        
          <input 
            id="insertArea"
      
            className="input"
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />

          <button onClick={this.handleBtnClick}>
            提交
          </button>
        </div>

        <ul>
          {this.getTodoItem()}
        </ul>

      </Fragment>
    )
  }

  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        <TodoItem 
        	key={item}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }
  
  // 1️⃣当 input 框里的值发生变化后,handleInputChange 里的函数体将会执行;
  handleInputChange(e) {
    console.log(e.target) // 2️⃣我们可以在控制台打印出 e.target,看看它到底是个什么东西?
    
    const value = e.target.value // 1️⃣-①:首先拿到 input 框里的 value 值;
    
    this.setState(() => ({
      inputValue: value
    })) // 1️⃣-②:然后用 setState 去修改"数据项"inputValue 里的值;
  }


  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }))
  }

  handleItemDelete(index) { 
    this.setState((prevState) => {
      const list = [...prevState.list]
      list.splice(index, 1)
        
      return {list}
 
    })
  }
}

export default TodoList;

当我们在页面的 input 框输入内容时,查看页面控制台, console.log(e.target) 给我们打印出了 input 框的 DOM 节点:

由实践可知,在 React 里,我们可以用 e.target 获取到事件对应"元素"的 DOM 节点。

在 React 里,也可以用另外一种方式(ref)来获取元素对应的 DOM。

2 ref

紧接上边的代码,我们用 ref 去做些修改:

jsx 复制代码
import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {  
      inputValue: "", 
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() {  
    
    return(
      <Fragment>
        <div>
          <label htmlFor="insertArea">请输入要进行的事项:</label>    
        
          <input 
            id="insertArea"
      
            className="input"
            value={this.state.inputValue}
            onChange={this.handleInputChange}

						ref = {(input) => {this.input = input}}
          />
						{/*
             1️⃣首先,给 input 元素增加一个 ref 属性,它等于一个"箭头函数"。
             "箭头函数"的格式为:
             1️⃣-①:箭头的左边,"箭头函数"会自动接收到一个"参数",名字可以随便取(我们取名为 input);
             1️⃣-②:箭头的右边,是一个固定写法 this.input = input ;
             1️⃣-③:❗️❗️❗️这整行代码的意思为------我构建了一个"引用"ref,这个"引用"叫做 this.input,
             它指向 input 框对应的 DOM 节点。
              */}						

						{/*
             2️⃣接着,input 框绑定了一个 handleInputChange 方法。
             故,我们去下边找到这个方法,也用 ref 进行相应改写;
              */}


          <button onClick={this.handleBtnClick}>
            提交
          </button>
        </div>

        <ul>
          {this.getTodoItem()}
        </ul>

      </Fragment>
    )
  }

  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        <TodoItem 
        	key={item}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }
  
  handleInputChange() {
    /*
    3️⃣在 handleInputChange 方法里,
    既然上边通过 ref 方式获取到了事件对应元素的 DOM 节点(this.input),
    那这里就可以进行相应地改写:
     */
    const value = this.input.value
    
    /*
    4️⃣并把之前用 e.target 获取事件对应元素的 DOM 节点的方式注释掉(
    注意把上边给"方法"传入的参数 e 也给去掉)。
    const value = e.target.value 
     */
    
    this.setState(() => ({
      inputValue: value
    }))
  }


  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }))
  }

  handleItemDelete(index) { 
    this.setState((prevState) => {
      const list = [...prevState.list]
      list.splice(index, 1)
        
      return {list}
 
    })
  }
}

export default TodoList;

5️⃣查看下页面效果:

以上效果正常运行,无报错。可见用 ref 来获取 DOM 节点是可以被正常使用的。但,也请你不要滥用!React 是"数据驱动"的框架,非特殊情况下,不要去主动操作 DOM。

况且,用 ref 时,一不小心就会出 bug,比如和 setState 合用。

3 refsetState 合用

紧接上边的代码,打开 TodoList.js 文件:

jsx 复制代码
import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {  
      inputValue: "", 
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() {  
    
    return(
      <Fragment>
        <div>
          <label htmlFor="insertArea">请输入要进行的事项:</label>    
        
          <input 
            id="insertArea"
      
            className="input"
            value={this.state.inputValue}
            onChange={this.handleInputChange}

            ref = {(input) => {this.input = input}}
          />

				  {/*
           1️⃣页面上有一个 button 按钮,当我们"点击"时,
           它会去执行 handleBtnClick 方法;
            */}
          <button onClick={this.handleBtnClick}>
            提交
          </button>

        </div>

        <ul ref={(ul) => {this.ul = ul}}> {/*
        																	 3️⃣-①:首先,在 ul 上增加一个 ref 属性(注意写法),
        																	 这样的话,当 ul 被创建后,this.ul 就指向 ul 对应
                                           的真实 DOM 节点;
                                            */}
          {this.getTodoItem()}
        </ul>

      </Fragment>
    )
  }

  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        <TodoItem 
        	key={item}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }
  
  handleInputChange() {
    const value = this.input.value
    
    this.setState(() => ({
      inputValue: value
    }))
  }

  
  /*
  2️⃣handleBtnClick 方法里的逻辑为:给"数据项"list 增加一个数据,
  并把 input 框清空;
   */
  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }))
    
    /*
    3️⃣此时,我有一个需求:在上边的逻辑执行完后,
    我想获取到"数据项"list 对应的 DOM 节点。应该怎么做呢?
     */
    
    // 3️⃣-②:然后,试着在控制台打印出相应节点:
    console.log(this.ul)
  }

  handleItemDelete(index) { 
    this.setState((prevState) => {
      const list = [...prevState.list]
      list.splice(index, 1)
        
      return {list}
 
    })
  }
}

export default TodoList;

3️⃣-③:查看页面控制台的打印信息;

4️⃣❓在这基础上,我有了新的需求:我想打印出 ul 下的 div 长度?

紧接上边代码:

jsx 复制代码
import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {  
      inputValue: "", 
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() {  
    
    return(
      <Fragment>
        <div>
          <label htmlFor="insertArea">请输入要进行的事项:</label>    
        
          <input 
            id="insertArea"
      
            className="input"
            value={this.state.inputValue}
            onChange={this.handleInputChange}

            ref = {(input) => {this.input = input}}
          />
          <button onClick={this.handleBtnClick}>
            提交
          </button>

        </div>

        <ul ref={(ul) => {this.ul = ul}}> 
          {this.getTodoItem()}
        </ul>

      </Fragment>
    )
  }

  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        <TodoItem 
        	key={item}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }
  
  handleInputChange() {
    const value = this.input.value
    
    this.setState(() => ({
      inputValue: value
    }))
  }

  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }))
    
    /*
    4️⃣-②:既然想打印 ul 中 div 的长度,
    我们可以借助原生 JS 中的相关方法:
     */   
    console.log(this.ul.querySelectorAll("div").length)
    
    
    /*
    4️⃣-①:先把这行代码注释掉,我们有了新的需求;
    console.log(this.ul)
     */
  }

  handleItemDelete(index) { 
    this.setState((prevState) => {
      const list = [...prevState.list]
      list.splice(index, 1)
        
      return {list}
 
    })
  }
}

export default TodoList;

4️⃣-③:回到页面控制台查看 this.ul.querySelectorAll("div").length 打印出的 div 长度;

❌问题出现了:

  • 我输入 Oli 后,控制台预期应该打印出长度 1 ,它却打印出 0
  • 我再输入 qdywxs 后,控制台预期应该打印长度 2 ,它却打印出 1

即,控制台打印出的 div 长度总是比实际长度少 1 个

❓这是为什么呢?

答:

  1. 你不去操作 DOM 就没事, refsetState 结合使用,就会出现这种问题;
  2. 这个问题可以解吗?------可以,这个问题我们之前就涉及过。

React 中,setState 被设计成一个"异步函数"

即,在本例中,当调用 handleBtnClick 的时候, setState 中的函数体并不会立即执行,它会等一会儿再去执行。

console.log(this.ul.querySelectorAll("div").length) 很有可能是在 setState 底层运行之前去运行的(根据页面效果来看,确实是这样)。

❓这种情况怎么解呢?

setState 允许我们在它里边添加第二个参数(也是一个函数) ,即,将 console.log(this.ul.querySelectorAll("div").length) 以"函数"的形式传给 setState

javascript 复制代码
  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }), () => {
      console.log(this.ul.querySelectorAll("div").length)
    }) /*
    	 ❓第二个"函数"形式的"参数"什么时候执行呢?
       答:会等到 setState 这个"异步"的方法完全执行好了后,
       作为"回调函数"的第二个参数才会被执行。
        */
    
  }

回到页面,看下效果(打印出的 div 长度正确,页面也没报错):

祝好,qdywxs ♥ you!

相关推荐
随云6323 分钟前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
无知的小菜鸡6 分钟前
路由:ReactRouter
react.js
寻找09之夏1 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
非著名架构师1 小时前
js混淆的方式方法
开发语言·javascript·ecmascript
多多米10052 小时前
初学Vue(2)
前端·javascript·vue.js
敏编程2 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱2 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑2 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8562 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习2 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript