React全家桶笔记(三):React进阶 — 事件处理、表单与生命周期

2 阅读9分钟

React全家桶笔记(三):React进阶 — 事件处理、表单与生命周期

本篇涵盖 React 事件处理机制、受控/非受控组件、高阶函数与柯里化、生命周期(新旧对比)、Diffing 算法。这些是从"会用"到"理解原理"的关键跨越。 📺 对应张天禹react全家桶视频:P32 - P48


一、React 中的事件处理(P32)

1.1 事件处理机制

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 事件处理的两个要点

  1. React 使用的是自定义(合成)事件,而不是原生 DOM 事件 — 为了更好的兼容性
  2. React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素) — 为了高效

💡 实践建议:不要过度使用 ref。当发生事件的元素就是你要操作的元素时,可以通过 event.target 获取 DOM,不需要 ref。


二、受控组件与非受控组件(P33-P34)

2.1 非受控组件(P33)

表单数据在需要时才通过 ref "现取现用":

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 取:

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)

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>
    )
  }
}

概念解析

高阶函数:满足以下任一条件的函数

  • 接收的参数是一个函数(如 PromisesetTimeoutarr.map()
  • 返回值是一个函数(如上面的 saveFormData

函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

// 普通函数
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)

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)

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

constructor()                → 构造器
componentWillMount()         → 组件将要挂载
render()                     → 渲染
componentDidMount()          → 组件挂载完毕 ⭐ 常用

更新阶段(Update)— P39-P41

三种触发更新的方式:

方式1setState() — P39
shouldComponentUpdate()      → 组件是否应该更新(返回 true/false,默认返回 true)
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

方式2forceUpdate() — P40(强制更新,跳过 shouldComponentUpdate)
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

方式3:父组件重新 render — P41
componentWillReceiveProps()  → 组件将要接收新的 props ⚠️ 第一次不算
shouldComponentUpdate()      → 组件是否应该更新
componentWillUpdate()        → 组件将要更新
render()                     → 重新渲染
componentDidUpdate()         → 组件更新完毕

卸载阶段(Unmount)

componentWillUnmount()       → 组件将要卸载 ⭐ 常用

旧版生命周期总结(P42)

旧版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────┐
│  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 派生 state
  • getSnapshotBeforeUpdate — 在更新前获取快照

4.4 getDerivedStateFromProps(P44)

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)

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)

新版生命周期流程图:
┌─ 挂载时 ──────────────────────────────────────────────┐
│  constructor → getDerivedStateFromProps → render        │
│  → componentDidMount ⭐                                │
└────────────────────────────────────────────────────────┘
┌─ 更新时 ──────────────────────────────────────────────────────────┐
│  getDerivedStateFromProps → shouldComponentUpdate(true)             │
│  → render → getSnapshotBeforeUpdate → componentDidUpdate           │
└────────────────────────────────────────────────────────────────────┘
┌─ 卸载时 ──────────────────────────────────────────────┐
│  componentWillUnmount ⭐                               │
└────────────────────────────────────────────────────────┘

最重要的三个钩子

componentDidMount    → 组件挂载完毕
  常用于:发送网络请求、订阅消息、开启定时器

componentDidUpdate   → 组件更新完毕
  常用于:根据更新后的 props/state 做操作

componentWillUnmount → 组件将要卸载
  常用于:清除定时器、取消订阅、清理工作

🎯 面试高频:React 新旧生命周期的区别?

  1. 废弃了三个 Will 钩子(componentWillMount/Update/ReceiveProps)
  2. 新增了 getDerivedStateFromProps(从 props 派生 state)和 getSnapshotBeforeUpdate(更新前快照)
  3. 废弃原因:为 React 未来的异步渲染(Concurrent Mode)做准备
  4. 最常用的仍然是:componentDidMountcomponentWillUnmount

五、DOM 的 Diffing 算法(P48)

5.1 Diffing 算法的最小粒度

Diffing 算法对比的最小粒度是标签(节点) ,不是整棵树。

// 假设 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 的作用

// 用 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 的问题

  1. 效率问题:逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实 DOM 更新(界面没问题,但效率低)
  2. 严重 Bug:如果结构中包含输入类 DOM(input),会产生错误的 DOM 更新(输入框内容错位)

5.4 key 的选择原则

key 的选择:
├── 最好使用数据的唯一标识(id、手机号、身份证号等)
├── 如果只是简单的展示数据(不涉及逆序操作),用 index 也可以
└── 绝对不要用 Math.random() 作为 key

🎯 面试高频:React/Vue 中 key 的作用和原理?

  1. key 是虚拟 DOM 对象的标识,在更新时起关键作用

  2. 当数据变化时,React 生成新的虚拟 DOM,然后与旧的进行 Diff 对比

  3. 对比规则:

    1. 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:

      • 内容没变 → 直接复用之前的真实 DOM
      • 内容变了 → 生成新的真实 DOM,替换掉旧的
    2. 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key:

      • 创建新的真实 DOM,渲染到页面

本章知识图谱

React 进阶
├── 事件处理
│   ├── 合成事件(非原生事件)
│   ├── 事件委托机制
│   └── event.target 可以省略 ref
├── 表单处理
│   ├── 非受控组件:ref 现用现取
│   └── 受控组件:onChange + state 实时存储(推荐)
├── 高阶函数与柯里化
│   ├── 高阶函数:参数或返回值是函数
│   ├── 柯里化:多次接收参数,最后统一处理
│   └── 替代方案:箭头函数包裹
├── 生命周期
│   ├── 旧版:Will 系列 + Did 系列
│   ├── 新版:废弃 3 个 Will,新增 2get
│   ├── 最常用:componentDidMount / componentWillUnmount
│   └── 废弃原因:为异步渲染(Concurrent Mode)铺路
└── Diffing 算法
    ├── 最小对比粒度:标签(节点)
    ├── key 是虚拟 DOM 的标识
    ├── 用 id 作 key(推荐)
    └── 用 index 作 key 的两个问题:效率低 + 输入框错位

📌 下一篇:[React全家桶笔记(四):React脚手架与TodoList实战] 将进入工程化开发阶段,学习 Create React App 脚手架和第一个完整的实战案例。