Skip to content

项目目录精简(Day4 4/4) ​

本页对应 React + TypeScript + 医通项目,共 5 段视频,约 46 分钟。

学习目标 ​

  • 能按“创建 ref 对象、绑定 ref 属性、通过 current 获取真实 DOM”三步,在 React 类组件中获取 input 元素并读取其 value。
  • 能解释受控组件的含义,并实现将组件 state 绑定到表单元素的 value 属性,说明表单变为只读及产生警告的原因。
  • 能为受控表单添加 onChange 事件,在回调中通过事件对象获取用户最新输入,使用 setState 更新状态,并正确处理 this 指向。
  • 能利用表单元素的 name 属性和对象中括号属性访问语法,封装一个通用的 change 处理函数,使多个表单元素复用同一套受控逻辑。
  • 能实现 radio 单选框和 checkbox 复选框的受控绑定,使用 checked 属性绑定状态,并通过 onChange 更新对应状态。

知识点 ​

本节说明:按转写稿整理,不补写未讲内容 ​

本节笔记标题是“13-4. 项目目录精简”,但转写稿实际讲解的是 ref 获取真实 DOM、受控组件、onChange 封装以及 radio/checkbox 的 checked 受控。笔记中没有出现目录精简的步骤,也没有图片和完整代码。为避免编造,下面按转写稿内容整理;标题与内容不一致之处先作说明,不擅自补写“项目目录精简”的操作。

为什么需要 ref:JSX 里也要拿真实 DOM ​

React 中我们写的是 JSX,平时操作的是虚拟 DOM。但有些特殊需求必须拿到真实 DOM,例如读取 input 的 value。过去可以 document.querySelector('input'),但在 React 组件里这种方式不合适。转写稿给出的思路是:通过 ref 在虚拟 DOM 和真实 DOM 之间建立引用。

怎么做分三步:第一步创建 ref 对象;第二步给目标元素绑定 ref 属性;第三步通过 ref.current 获取真实 DOM。易错点是:current 初始为 null,只有绑定并渲染后才有值;不要再用选择器绕过 React 去取。

jsx
import React, { Component, createRef } from 'react';

class App extends Component {
  constructor() {
    super();
    this.inputRef = createRef();
  }

  getValue = () => {
    console.log(this.inputRef.current.value);
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} />
        <button onClick={this.getValue}>获取输入值</button>
      </div>
    );
  }
}

调试时转写稿反复打印 this.inputRef 和 this.inputRef.current:前者是 ref 对象,未绑定时 current 为 null;绑定 input 后,current 就是该 input 的真实 DOM。易错点还有事件回调里的 this,后面受控组件会再次遇到。

受控组件:表单值被 state 控制 ​

受控组件是相对于表单元素说的:表单元素的值受到组件状态数据控制。为什么需要?因为表单值要和 state 绑定,用户输入、状态更新、页面显示形成统一数据源。怎么想?把 state 渲染到表单的 value 上;用户输入时通过 onChange 获取最新值并 setState。怎么做?

jsx
class FormControl extends Component {
  state = {
    userName: '艾硅谷',
    password: '123123'
  };

  handleChange = (e) => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  save = (e) => {
    e.preventDefault();
    console.log(this.state.userName, this.state.password);
  };

  render() {
    return (
      <form onSubmit={this.save}>
        <input
          name="userName"
          value={this.state.userName}
          onChange={this.handleChange}
        />
        <input
          name="password"
          value={this.state.password}
          onChange={this.handleChange}
        />
        <button type="submit">保存</button>
      </form>
    );
  }
}

易错点一:只给 value 赋值却不写 onChange,输入框会变成只读,并出现警告。转写稿的解释是:输入时真实 DOM 的 value 变了,但 state 没变,React 又按 state 把值改回去。易错点二:onSubmit 要 e.preventDefault() 阻止表单默认提交刷新。易错点三:事件回调里直接写 this.setState 可能报 Cannot read property 'setState' of undefined,因为回调调用时 this 不是组件实例;用箭头函数或 bind 解决。

用 name 合并多个 change 函数 ​

如果每个表单都写一个 change 函数,表单多时很冗余。转写稿的思路是:给每个 input 加 name,并让 name 与 state 字段同名;在 change 中用 e.target.name 取字段名,用 e.target.value 取最新值。对象属性名是变量时,必须用中括号语法:

jsx
handleChange = (e) => {
  this.setState({
    [e.target.name]: e.target.value
  });
};

易错点:[e.target.name] 不能写成 e.target.name 点语法;name 写错就和 state 字段对不上。这样 state 与表单值互相影响,转写稿称为“双向绑定”。

radio 与 checkbox:选中状态看 checked ​

单选框和复选框的选中状态不是用 value 控制,而是用 checked 受控。转写稿演示了 radio 的 checked 与 onChange:选中状态由 state 决定,用户点击触发 change,再更新 state。若只写 checked 不写 onChange,同样会只读、无法切换。checkbox 同理,核心是 checked 属性受控,change 中按需求读取 e.target.value 或 e.target.checked 并更新对应状态。转写稿未展开完整 checkbox 代码,这里不补造额外业务逻辑。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:ref 的基本使用(获取真实 DOM) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>ref 的基本使用</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { Component, createRef } = React;

    class App extends Component {
      constructor(props) {
        super(props);
        // 第一步:创建 ref 对象,并让它成为类组件的私有属性
        this.inputRef = createRef();
        this.getValue = this.getValue.bind(this);
      }

      // 第三步:通过 ref 对象的 current 属性获取真实 DOM
      getValue() {
        console.log('真实 DOM:', this.inputRef.current);
        console.log('用户输入的内容:', this.inputRef.current.value);
      }

      render() {
        return (
          <div>
            {/* 第二步:给要获取真实 DOM 的元素绑定 ref 属性 */}
            <input type="text" ref={this.inputRef} placeholder="请输入内容" />
            <button onClick={this.getValue}>获取 input 的值</button>
          </div>
        );
      }
    }

    ReactDOM.render(<App />, document.getElementById('root'));
  </script>
</body>
</html>

关键行与易错点

  • createRef() 必须在组件里创建(构造函数中赋值给 this.inputRef),不能写在 render 里,否则每次渲染都是新对象。
  • ref={this.inputRef} 只是“绑定”,此时 current 还是 null;只有渲染完成后才通过 this.inputRef.current 拿到真实 DOM。
  • 不能再用 document.querySelector('input') 的思路去拿节点,JSX 里拿到的是虚拟 DOM,要真实 DOM 只能走 ref。
  • 事件回调必须 bind(this),否则 this.inputRef 是 undefined。

示例二:受控组件(value 绑定 state + onChange) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>受控组件</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { Component } = React;

    class FormControl extends Component {
      constructor(props) {
        super(props);
        this.state = {
          userName: '艾硅谷',
          password: '123123'
        };
        this.changeUserName = this.changeUserName.bind(this);
        this.changePassword = this.changePassword.bind(this);
        this.save = this.save.bind(this);
      }

      // 获取用户最新输入,并给状态赋值
      changeUserName(e) {
        this.setState({ userName: e.target.value });
      }

      changePassword(e) {
        this.setState({ password: e.target.value });
      }

      save(e) {
        // 阻止表单默认行为:提交到 action 指定的地址并刷新页面
        e.preventDefault();
        console.log('最新的姓名:', this.state.userName);
        console.log('最新的密码:', this.state.password);
      }

      render() {
        const { userName, password } = this.state;
        return (
          <form onSubmit={this.save}>
            <p>
              姓名:
              <input type="text" value={userName} onChange={this.changeUserName} />
            </p>
            <p>
              密码:
              <input type="password" value={password} onChange={this.changePassword} />
            </p>
            <button type="submit">保存</button>
          </form>
        );
      }
    }

    ReactDOM.render(<FormControl />, document.getElementById('root'));
  </script>
</body>
</html>

关键行与易错点

  • 一旦 value={状态数据},表单元素就“受控”了,变成只读,用户输入无效,控制台还会警告“value without onChange”。
  • 必须补 onChange:在回调里用 e.target.value 拿到用户最新输入,再 setState 改状态;状态一变,render 重新执行,value 读到新值,输入才显示出来。
  • 状态和表单值互相影响,就是“双向绑定”;保存时直接从 this.state 读最新值即可。
  • 两个高频坑:① 回调没 bind(this),报 Cannot read property 'setState' of undefined;② 忘了 e.preventDefault(),点保存会提交并刷新页面。
  • 可提交按钮:type="submit"、不写 type、<input type="submit">;type="button" 不能提交。

示例三:change 函数封装 + radio/checkbox 通过 checked 受控 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>change 函数封装</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { Component } = React;

    class FormControl extends Component {
      constructor(props) {
        super(props);
        this.state = {
          userName: '艾硅谷',
          password: '123123',
          sex: '0',      // radio 用字符串值
          agree: false   // checkbox 用布尔值
        };
        this.change = this.change.bind(this);
        this.save = this.save.bind(this);
      }

      // 所有表单共用同一个 change 函数
      change(e) {
        const { name, value, type, checked } = e.target;
        // 中括号语法:把 name 当作变量作为属性名
        this.setState({
          [name]: type === 'checkbox' ? checked : value
        });
      }

      save(e) {
        e.preventDefault();
        console.log(this.state);
      }

      render() {
        const { userName, password, sex, agree } = this.state;
        return (
          <form onSubmit={this.save}>
            <p>
              姓名:
              <input type="text" name="userName" value={userName} onChange={this.change} />
            </p>
            <p>
              密码:
              <input type="password" name="password" value={password} onChange={this.change} />
            </p>
            <p>
              性别:
              <input type="radio" name="sex" value="0" checked={sex === '0'} onChange={this.change} /> 女
              <input type="radio" name="sex" value="1" checked={sex === '1'} onChange={this.change} /> 男
            </p>
            <p>
              <input type="checkbox" name="agree" checked={agree} onChange={this.change} /> 同意协议
            </p>
            <button type="submit">保存</button>
          </form>
        );
      }
    }

    ReactDOM.render(<FormControl />, document.getElementById('root'));
  </script>
</body>
</html>

关键行与易错点

  • 封装的三个前提:给每个表单加 name 属性、name 的取值和 state 里的键名保持一致、用 e.target.name 动态取属性名。
  • 属性名是变量时必须用中括号语法 { [name]: value },写成 { name: value } 会变成一个叫 name 的新字段。
  • radio/checkbox 控制的是 checked 而不是 value:checked={sex === '0'},状态变了选中状态才跟着变。
  • radio 用 value 区分选项值,checkbox 用 checked(布尔)切换;注意 e.target.value 永远是字符串,需要数字时得自己转。
  • 一个 change 函数配 bind(this),表单再多也不用重复写回调;漏写 name 或名字与 state 键名不一致,是最常见的“点了没反应”原因。

常见错误 ​

  1. 想拿输入框的值,却去写 document.querySelector('input') 现象:拿到的不是 JSX 里那个节点,或者拿到的值永远滞后一步。 原因:JSX 渲染的是虚拟 DOM,真实节点由 React 管理,选择器绕过了 React 的渲染时机。 修法:createRef() 建对象 → 元素上写 ref={this.xxxRef} → 用 this.xxxRef.current 取真实 DOM。

  2. 打印 this.inputRef.current 是 null 现象:控制台一直输出 null,绑定了也没用。 原因:读取的时机太早(在 render 里或挂载前),此时节点还没生成;或者 ref 属性漏写、写到了错误的元素上。 修法:把读取放到事件回调、生命周期或异步回调里;确认 ref 属性确实挂在了目标元素上。

  3. 事件回调里 this.setState 报“Cannot read property 'setState' of undefined” 现象:一输入内容或一点保存就报错,说在 undefined 上读 setState。 原因:事件回调是由浏览器/React 调用的,调用时 this 不是组件实例。 修法:onChange={this.handleChange.bind(this)},或在类里用箭头函数定义 handleChange = (e) => {...}。

  4. 给表单加了 value={this.state.xxx},但输入框敲不进去字,控制台还有警告 现象:光标能进,字符一闪就没了,同时出现“value without onChange”的警告。 原因:只给了值没给变更通道,输入被 setState 之外的方式改了,下一帧又被 state 覆盖回来,于是变成只读。 修法:补 onChange,在回调里 e.target.value 取最新输入并 setState 回去;若只想给初始值,用 defaultValue。

  5. 表单字段一多,就复制粘贴出十几个 changeUserName / changePassword / changeAge... 现象:处理函数成片重复,改一处要改十几处。 原因:把“字段名”硬编码进了函数名和 setState 的对象字面量里。 修法:让每个表单的 name 与 state 的键同名,统一写一个 handleChange,用 this.setState({ [e.target.name]: e.target.value }) 计算属性名收口。

  6. 单选框/复选框用 value 去控制选中状态 现象:state 改了,界面上勾选状态纹丝不动,也点不动。 原因:radio/checkbox 的“选中”语义由 checked 承载,value 只是提交值。 修法:checked={this.state.sex === 'male'}(复选框用数组包含判断),同样配 onChange。

  7. 点击保存后页面整个刷新,state 全部回到初始值 现象:数据刚填好,一点按钮全没了,控制台日志一闪而过。 原因:submit 按钮触发了 form 的默认提交行为,跳转到 action 或当前地址。 修法:在 onSubmit 回调里第一行 e.preventDefault(),再做业务处理。

练习 ​

  1. 在组件里放一个 input 和一个按钮,点击按钮时让输入框自动获得焦点,并在控制台打印当前输入内容。要求全程不使用 document.querySelector。
  2. 做一个姓名 + 密码的登录表单,两个字段都改成受控,点击保存时阻止默认提交行为,打印出最新的 state。
  3. 把上题扩展成姓名、密码、邮箱、手机号四个字段,只保留一个 handleChange,靠 name 属性和计算属性名复用。写完后自测:删掉任意一个字段的 name,说明会出现什么现象。
  4. 增加性别单选(男/女)和爱好多选(读书/运动/音乐),用 checked 实现受控,提交时能拿到性别字符串和爱好数组。
  5. 排错题:下面这段代码有三处问题,指出并改正。
    jsx
    state = &#123; kw: '' &#125;
    handleChange(e) &#123; this.setState(&#123; kw: e.target.value &#125;) &#125;
    render() &#123;
      return &lt;div&gt;
        &lt;input value=&#123;this.state.kw&#125; onChange=&#123;this.handleChange&#125; /&gt;
        &lt;button onClick=&#123;this.save&#125;&gt;保存&lt;/button&gt;
      &lt;/div&gt;
    &#125;

原视频对照 ​

#视频时长
1ref的基本使用08:48
2受控组件24:59
3受控表单-change函数封装05:01
4对象属性操作复习02:44
5radio单选框-复选框都是通过checked属性值受控05:04

小结 ​

  • ref 是逃生舱:只有聚焦、测量、播放媒体这类“必须碰真实 DOM”的需求才用它,能用 state 驱动的场景一律不用。
  • 受控的核心不是“写了 value”,而是“值的唯一来源是 state”——value 只是 state 的一次投影。
  • value 与 onChange 是一对:少任何一个,要么只读,要么警告,要么数据流断掉。
  • 类组件里凡是作为回调传给 JSX 的方法,都要先解决 this 归属,再谈逻辑;这一条对 onChange、onSubmit、onClick 一视同仁。
  • 表单多的时候不要靠“多写几个函数”解决,而要靠“命名对齐 + 计算属性名”把 N 个函数压成 1 个。
  • 不同表单元素的受控属性不同:文本类看 value,选择类看 checked,搞混就点不动。
  • 表单提交的第一行永远是 preventDefault,否则你调试的一切都会被一次页面刷新清空。
  • 排查顺序固定:ref.current 有没有值 → this 指向对不对 → state 是否真的更新了 → 受控属性用对没有。