主题
项目目录精简(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 代码,这里不补造额外业务逻辑。
课堂画面





代码示例
示例一: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 键名不一致,是最常见的“点了没反应”原因。
常见错误
想拿输入框的值,却去写
document.querySelector('input')现象:拿到的不是 JSX 里那个节点,或者拿到的值永远滞后一步。 原因:JSX 渲染的是虚拟 DOM,真实节点由 React 管理,选择器绕过了 React 的渲染时机。 修法:createRef()建对象 → 元素上写ref={this.xxxRef}→ 用this.xxxRef.current取真实 DOM。打印
this.inputRef.current是null现象:控制台一直输出null,绑定了也没用。 原因:读取的时机太早(在 render 里或挂载前),此时节点还没生成;或者ref属性漏写、写到了错误的元素上。 修法:把读取放到事件回调、生命周期或异步回调里;确认ref属性确实挂在了目标元素上。事件回调里
this.setState报“Cannot read property 'setState' of undefined” 现象:一输入内容或一点保存就报错,说在undefined上读setState。 原因:事件回调是由浏览器/React 调用的,调用时this不是组件实例。 修法:onChange={this.handleChange.bind(this)},或在类里用箭头函数定义handleChange = (e) => {...}。给表单加了
value={this.state.xxx},但输入框敲不进去字,控制台还有警告 现象:光标能进,字符一闪就没了,同时出现“value without onChange”的警告。 原因:只给了值没给变更通道,输入被setState之外的方式改了,下一帧又被 state 覆盖回来,于是变成只读。 修法:补onChange,在回调里e.target.value取最新输入并setState回去;若只想给初始值,用defaultValue。表单字段一多,就复制粘贴出十几个
changeUserName / changePassword / changeAge...现象:处理函数成片重复,改一处要改十几处。 原因:把“字段名”硬编码进了函数名和setState的对象字面量里。 修法:让每个表单的name与 state 的键同名,统一写一个handleChange,用this.setState({ [e.target.name]: e.target.value })计算属性名收口。单选框/复选框用
value去控制选中状态 现象:state改了,界面上勾选状态纹丝不动,也点不动。 原因:radio/checkbox 的“选中”语义由checked承载,value只是提交值。 修法:checked={this.state.sex === 'male'}(复选框用数组包含判断),同样配onChange。点击保存后页面整个刷新,state 全部回到初始值 现象:数据刚填好,一点按钮全没了,控制台日志一闪而过。 原因:
submit按钮触发了 form 的默认提交行为,跳转到action或当前地址。 修法:在onSubmit回调里第一行e.preventDefault(),再做业务处理。
练习
- 在组件里放一个
input和一个按钮,点击按钮时让输入框自动获得焦点,并在控制台打印当前输入内容。要求全程不使用document.querySelector。 - 做一个姓名 + 密码的登录表单,两个字段都改成受控,点击保存时阻止默认提交行为,打印出最新的 state。
- 把上题扩展成姓名、密码、邮箱、手机号四个字段,只保留一个
handleChange,靠name属性和计算属性名复用。写完后自测:删掉任意一个字段的name,说明会出现什么现象。 - 增加性别单选(男/女)和爱好多选(读书/运动/音乐),用
checked实现受控,提交时能拿到性别字符串和爱好数组。 - 排错题:下面这段代码有三处问题,指出并改正。jsx
state = { kw: '' } handleChange(e) { this.setState({ kw: e.target.value }) } render() { return <div> <input value={this.state.kw} onChange={this.handleChange} /> <button onClick={this.save}>保存</button> </div> }
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | ref的基本使用 | 08:48 |
| 2 | 受控组件 | 24:59 |
| 3 | 受控表单-change函数封装 | 05:01 |
| 4 | 对象属性操作复习 | 02:44 |
| 5 | radio单选框-复选框都是通过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 是否真的更新了 → 受控属性用对没有。