React全家桶笔记(三):React进阶 --- 事件处理、表单与生命周期
本篇涵盖 React 事件处理机制、受控/非受控组件、高阶函数与柯里化、生命周期(新旧对比)、Diffing 算法。这些是从"会用"到"理解原理"的关键跨越。 📺 对应张天禹react全家桶视频:P32 - P48
一、React 中的事件处理(P32)
1.1 事件处理机制
scala
class Demo extends React.Component {
myRef = React.createRef()
// 发生事件的元素正好是你要操作的元素 → 可以省略 ref
showData = (event) => {
alert(event.target.value)
}
render() {
return (
<div>
<input onBlur={this.showData} type="text" placeholder="失去焦点提示" />
</div>
)
}
}
React 事件处理的两个要点:
- React 使用的是自定义(合成)事件,而不是原生 DOM 事件 --- 为了更好的兼容性
- React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素) --- 为了高效
💡 实践建议 :不要过度使用 ref。当发生事件的元素就是你要操作的元素时,可以通过
event.target获取 DOM,不需要 ref。
二、受控组件与非受控组件(P33-P34)
2.1 非受控组件(P33)
表单数据在需要时才通过 ref "现取现用":
scala
class Login extends React.Component {
handleSubmit = (event) => {
event.preventDefault() // 阻止表单默认提交行为
const { username, password } = this
alert(`用户名:${username.value},密码:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username" />
密码:<input ref={c => this.password = c} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
特点:输入类 DOM 的值是"现用现取"的,页面中的表单数据由 DOM 自身管理。
2.2 受控组件(P34)--- 推荐
表单数据随着输入实时维护到 state 中,需要时从 state 取:
javascript
class Login extends React.Component {
state = {
username: '',
password: ''
}
saveUsername = (event) => {
this.setState({ username: event.target.value })
}
savePassword = (event) => {
this.setState({ password: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
alert(`用户名:${username},密码:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username" />
密码:<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
特点:输入类 DOM 的值实时存入 state,等于 Vue 中的双向绑定效果。推荐使用,因为可以省略 ref。
🎯 面试高频:受控组件 vs 非受控组件
- 受控组件:表单数据由 React 的 state 管理,输入即存储,类似 Vue 的 v-model
- 非受控组件:表单数据由 DOM 自身管理,需要时通过 ref 获取
- 推荐受控组件,因为数据集中管理,且不需要大量 ref
三、高阶函数与函数柯里化(P35-P36)
3.1 问题引出
上面的受控组件中,每个表单项都要写一个 saveXxx 方法,如果有 20 个表单项就要写 20 个方法,太冗余了。
3.2 用柯里化优化(P35)
javascript
class Login extends React.Component {
state = { username: '', password: '' }
// 高阶函数 + 柯里化:返回一个函数作为事件回调
saveFormData = (dataType) => {
return (event) => {
this.setState({ [dataType]: event.target.value })
}
}
handleSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
alert(`用户名:${username},密码:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
{/* 注意:onChange 的值必须是一个函数,这里 saveFormData('username') 的返回值就是一个函数 */}
用户名:<input onChange={this.saveFormData('username')} type="text" />
密码:<input onChange={this.saveFormData('password')} type="password" />
<button>登录</button>
</form>
)
}
}
概念解析:
高阶函数:满足以下任一条件的函数
- 接收的参数是一个函数(如
Promise、setTimeout、arr.map()) - 返回值是一个函数(如上面的
saveFormData)
函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
javascript
// 普通函数
function sum(a, b, c) { return a + b + c }
sum(1, 2, 3) // 6
// 柯里化
function sum(a) {
return (b) => {
return (c) => {
return a + b + c
}
}
}
sum(1)(2)(3) // 6
3.3 不用柯里化的写法(P36)
scala
class Login extends React.Component {
state = { username: '', password: '' }
saveFormData = (dataType, event) => {
this.setState({ [dataType]: event.target.value })
}
render() {
return (
<form onSubmit={this.handleSubmit}>
{/* 用箭头函数包一层,在回调中自己调用 saveFormData */}
用户名:<input onChange={event => this.saveFormData('username', event)} type="text" />
密码:<input onChange={event => this.saveFormData('password', event)} type="password" />
<button>登录</button>
</form>
)
}
}
🔗 概念扩展 :两种写法的本质 不管是柯里化还是箭头函数包裹,核心目的都是一样的:确保 onChange 的值是一个函数,同时能把额外的参数(dataType)传进去。
四、生命周期(P37-P47)
4.1 引出生命周期(P37)
javascript
class Life extends React.Component {
state = { opacity: 1 }
// 组件挂载完毕后调用
componentDidMount() {
this.timer = setInterval(() => {
let { opacity } = this.state
opacity -= 0.1
if (opacity <= 0) opacity = 1
this.setState({ opacity })
}, 200)
}
// 组件将要卸载时调用 --- 适合做收尾工作(清除定时器、取消订阅等)
componentWillUnmount() {
clearInterval(this.timer)
}
death = () => {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
render() {
return (
<div>
<h2 style={{opacity: this.state.opacity}}>React 学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
生命周期(又叫生命周期回调函数、生命周期钩子函数):React 组件从创建到销毁会经历一系列特定阶段,React 会在特定时刻调用特定的方法,这些方法就是生命周期钩子。
4.2 生命周期(旧)--- React 16 之前(P38-P42)
挂载阶段(Mount)--- P38:
scss
constructor() → 构造器
componentWillMount() → 组件将要挂载
render() → 渲染
componentDidMount() → 组件挂载完毕 ⭐ 常用
更新阶段(Update)--- P39-P41:
三种触发更新的方式:
scss
方式1:setState() --- P39
shouldComponentUpdate() → 组件是否应该更新(返回 true/false,默认返回 true)
componentWillUpdate() → 组件将要更新
render() → 重新渲染
componentDidUpdate() → 组件更新完毕
方式2:forceUpdate() --- P40(强制更新,跳过 shouldComponentUpdate)
componentWillUpdate() → 组件将要更新
render() → 重新渲染
componentDidUpdate() → 组件更新完毕
方式3:父组件重新 render --- P41
componentWillReceiveProps() → 组件将要接收新的 props ⚠️ 第一次不算
shouldComponentUpdate() → 组件是否应该更新
componentWillUpdate() → 组件将要更新
render() → 重新渲染
componentDidUpdate() → 组件更新完毕
卸载阶段(Unmount) :
scss
componentWillUnmount() → 组件将要卸载 ⭐ 常用
旧版生命周期总结(P42) :
scss
旧版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────┐
│ constructor → componentWillMount → render │
│ → componentDidMount │
└────────────────────────────────────────────┘
┌─ 更新时 ──────────────────────────────────────────────────┐
│ 父组件render → componentWillReceiveProps │
│ → shouldComponentUpdate(true) → componentWillUpdate │
│ → render → componentDidUpdate │
│ │
│ setState → shouldComponentUpdate(true) │
│ → componentWillUpdate → render → componentDidUpdate │
│ │
│ forceUpdate → componentWillUpdate → render │
│ → componentDidUpdate │
└────────────────────────────────────────────────────────────┘
┌─ 卸载时 ──────────────────────────────────┐
│ componentWillUnmount │
└────────────────────────────────────────────┘
4.3 对比新旧生命周期(P43)
React 17+ 中,三个带 Will 的钩子被标记为不安全(UNSAFE),需要加 UNSAFE_ 前缀才能使用:
❌ componentWillMount → UNSAFE_componentWillMount
❌ componentWillUpdate → UNSAFE_componentWillUpdate
❌ componentWillReceiveProps → UNSAFE_componentWillReceiveProps
为什么废弃? 这三个钩子经常被误用/滥用,在 React 未来的异步渲染(Fiber)中可能会出问题。
新版生命周期新增了两个钩子:
getDerivedStateFromProps--- 从 props 派生 stategetSnapshotBeforeUpdate--- 在更新前获取快照
4.4 getDerivedStateFromProps(P44)
scala
class Count extends React.Component {
state = { count: 0 }
// 注意:这是一个 static 方法,接收 props 和 state
// 返回一个状态对象或 null
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps', props, state)
// 返回的对象会与 state 合并
// 如果返回 null,则不影响 state
return null
}
render() {
return <h1>当前求和为:{this.state.count}</h1>
}
}
使用场景:state 的值在任何时候都取决于 props 时使用(极少用)。一旦使用,state 就会被 props "控制"住。
⚠️ 这个钩子使用场景非常罕见,了解即可。
4.5 getSnapshotBeforeUpdate(P45-P46)
javascript
class NewsList extends React.Component {
state = { newsArr: [] }
componentDidMount() {
setInterval(() => {
const { newsArr } = this.state
const news = `新闻${newsArr.length + 1}`
this.setState({ newsArr: [news, ...newsArr] })
}, 1000)
}
// 在更新之前获取快照(DOM 更新前的信息)
// 返回值会作为 componentDidUpdate 的第三个参数
getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight
}
componentDidUpdate(prevProps, prevState, snapshotValue) {
// snapshotValue 就是 getSnapshotBeforeUpdate 的返回值
// 用来保持滚动位置不变
this.refs.list.scrollTop += this.refs.list.scrollHeight - snapshotValue
}
render() {
return (
<div ref="list" className="list">
{this.state.newsArr.map((n, index) => (
<div key={index} className="news">{n}</div>
))}
</div>
)
}
}
使用场景 :在 DOM 更新前捕获一些信息(如滚动位置),传递给 componentDidUpdate 使用。
4.6 新版生命周期总结(P47)
kotlin
新版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────────────────┐
│ constructor → getDerivedStateFromProps → render │
│ → componentDidMount ⭐ │
└────────────────────────────────────────────────────────┘
┌─ 更新时 ──────────────────────────────────────────────────────────┐
│ getDerivedStateFromProps → shouldComponentUpdate(true) │
│ → render → getSnapshotBeforeUpdate → componentDidUpdate │
└────────────────────────────────────────────────────────────────────┘
┌─ 卸载时 ──────────────────────────────────────────────┐
│ componentWillUnmount ⭐ │
└────────────────────────────────────────────────────────┘
最重要的三个钩子:
bash
componentDidMount → 组件挂载完毕
常用于:发送网络请求、订阅消息、开启定时器
componentDidUpdate → 组件更新完毕
常用于:根据更新后的 props/state 做操作
componentWillUnmount → 组件将要卸载
常用于:清除定时器、取消订阅、清理工作
🎯 面试高频:React 新旧生命周期的区别?
- 废弃了三个
Will钩子(componentWillMount/Update/ReceiveProps) - 新增了
getDerivedStateFromProps(从 props 派生 state)和getSnapshotBeforeUpdate(更新前快照) - 废弃原因:为 React 未来的异步渲染(Concurrent Mode)做准备
- 最常用的仍然是:
componentDidMount、componentWillUnmount
五、DOM 的 Diffing 算法(P48)
5.1 Diffing 算法的最小粒度
Diffing 算法对比的最小粒度是标签(节点) ,不是整棵树。
javascript
// 假设 state 中 time 每秒更新一次
render() {
return (
<div>
<h1>Hello</h1>
<span>
现在是:{this.state.time}
</span>
</div>
)
}
// Diffing 对比时:
// <h1>Hello</h1> → 没变,不更新
// <span>现在是:xxx</span> → 内容变了,只更新这个 span
5.2 key 的作用
typescript
// 用 index 作为 key 的问题演示
// 初始数据:
// { id: 1, name: '小张', age: 18 }
// { id: 2, name: '小李', age: 19 }
// 初始虚拟 DOM(用 index 作 key):
<li key={0}>小张---18 <input type="text"/></li>
<li key={1}>小李---19 <input type="text"/></li>
// 在头部插入 { id: 3, name: '小王', age: 20 } 后:
<li key={0}>小王---20 <input type="text"/></li> // key=0 对比:内容变了,更新!
<li key={1}>小张---18 <input type="text"/></li> // key=1 对比:内容变了,更新!
<li key={2}>小李---19 <input type="text"/></li> // key=2:新增
// 如果用 id 作 key:
<li key={3}>小王---20 <input type="text"/></li> // key=3:新增,只创建这一个
<li key={1}>小张---18 <input type="text"/></li> // key=1 对比:没变,复用!
<li key={2}>小李---19 <input type="text"/></li> // key=2 对比:没变,复用!
5.3 用 index 作为 key 的问题
- 效率问题:逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实 DOM 更新(界面没问题,但效率低)
- 严重 Bug:如果结构中包含输入类 DOM(input),会产生错误的 DOM 更新(输入框内容错位)
5.4 key 的选择原则
vbnet
key 的选择:
├── 最好使用数据的唯一标识(id、手机号、身份证号等)
├── 如果只是简单的展示数据(不涉及逆序操作),用 index 也可以
└── 绝对不要用 Math.random() 作为 key
🎯 面试高频:React/Vue 中 key 的作用和原理?
-
key 是虚拟 DOM 对象的标识,在更新时起关键作用
-
当数据变化时,React 生成新的虚拟 DOM,然后与旧的进行 Diff 对比
-
对比规则:
-
旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:
- 内容没变 → 直接复用之前的真实 DOM
- 内容变了 → 生成新的真实 DOM,替换掉旧的
-
旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key:
- 创建新的真实 DOM,渲染到页面
-
本章知识图谱
vbnet
React 进阶
├── 事件处理
│ ├── 合成事件(非原生事件)
│ ├── 事件委托机制
│ └── event.target 可以省略 ref
├── 表单处理
│ ├── 非受控组件:ref 现用现取
│ └── 受控组件:onChange + state 实时存储(推荐)
├── 高阶函数与柯里化
│ ├── 高阶函数:参数或返回值是函数
│ ├── 柯里化:多次接收参数,最后统一处理
│ └── 替代方案:箭头函数包裹
├── 生命周期
│ ├── 旧版:Will 系列 + Did 系列
│ ├── 新版:废弃 3 个 Will,新增 2 个 get
│ ├── 最常用:componentDidMount / componentWillUnmount
│ └── 废弃原因:为异步渲染(Concurrent Mode)铺路
└── Diffing 算法
├── 最小对比粒度:标签(节点)
├── key 是虚拟 DOM 的标识
├── 用 id 作 key(推荐)
└── 用 index 作 key 的两个问题:效率低 + 输入框错位
📌 下一篇:[React全家桶笔记(四):React脚手架与TodoList实战] 将进入工程化开发阶段,学习 Create React App 脚手架和第一个完整的实战案例。