Skip to content

类实例、this 与 setState 队列 ​

函数组件每次调用会建立本次渲染的局部变量;类组件把当前 props 和 state 放在一个由 React 管理的实例上。两种方式描述的是同一棵组件树,都受类型、位置和 key 的身份规则约束。React 19 仍支持类组件;本篇先学会在类中维护行为,再讨论迁移,不能把看到 class 当成必须降级 React 的理由。

从一个能点击的实例开始 ​

本章沿用全书环境,每个完整例子独立替换 src/App.jsx,保留 createRoot 与入口 StrictMode。类声明属于 JavaScript:extends Component 继承 React 的更新接口,render() 返回 JSX,this.props 是输入,this.state 是状态对象。若不熟悉class/extends/super,先读JavaScript类与继承。

下面有两个独立计数器。先只点左边,再改变左边的 key。观察“实例内状态”与“父组件数据”何时分别变化。

jsx
import { Component, useState } from 'react'

export class Counter extends Component {
  state = { count: 0, note: '保留此字段' }

  addOne = () => {
    this.setState(previous => ({ count: previous.count + 1 }))
  }

  addThreeObjects = () => {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
  }

  addThreeFunctions = () => {
    this.setState(previous => ({ count: previous.count + 1 }))
    this.setState(previous => ({ count: previous.count + 1 }))
    this.setState(previous => ({ count: previous.count + 1 }))
  }

  render() {
    return <section aria-label={this.props.label}>
      <h2>{this.props.label}</h2>
      <output>{this.state.count}</output>
      <p>{this.state.note}</p>
      <button onClick={this.addOne}>加一</button>
      <button onClick={this.addThreeObjects}>三个对象更新</button>
      <button onClick={this.addThreeFunctions}>三个函数更新</button>
    </section>
  }
}

export default function App() {
  const [revision, setRevision] = useState(0)
  const [step, setStep] = useState(2)
  return <main>
    <button onClick={() => setRevision(n => n + 1)}>重建左侧</button>
    <button onClick={() => setStep(value => value === 2 ? 3 : 2)}>切换步长(当前{step})</button>
    <Counter key={revision} label="左侧" step={step} />
    <Counter label="右侧" step={step} />
  </main>
}

初始两个output都是0。在左侧依次点击三个对象更新、三个函数更新,得到1、4;右侧仍为0。重建左侧后左侧回0。父组件重新渲染本身没有要求丢弃右侧实例,左侧key变化才建立了新身份。不要在函数组件里声明Counter类,否则每次父渲染都创建新类型,会把身份重置伪装成state更新失败。

队列里的对象与函数不是同一种请求 ​

三个对象表达式都在当前事件里立刻求值。count为0时,入队的是 {count:1} 三次。处理队列时每个对象与当时状态做浅合并,结果仍是1;未提及的note保留。三个函数则等到处理队列时取得各自前面的待定状态,依次算出2、3、4。这和第二章函数组件队列的因果相通,但返回值的合并规则不同:类setState合并对象顶层字段,useState setter替换该状态值。

浅合并不意味着深合并。若state里有 {profile:{name:'Ada', city:'杭州'}},更新 {profile:{name:'Lin'}} 会替换整个profile,city丢失。应返回 {profile:{...previous.profile,name:'Lin'}}。同样不能修改 this.state.count++ 来请求更新;React没有收到合规更新,旧快照也被污染。

updater必须纯粹。不能在它里面发请求、埋点或修改外部数组,因为开发检查或渲染重试可能重新求值。需要在更新提交后操作,可以给setState第二个参数:在addOne里改为 this.setState(p => ({count:p.count+1}), () => console.log(this.state.count))。回调读取提交后的状态;它不是Promise,await this.setState(...) 没有“等提交”的合同。也不要靠调用后立刻读取this.state推断提交已完成。

this 为什么会丢失 ​

上例 addOne = () => ... 是实例字段,箭头函数捕获该实例的this。普通方法 addOne() { ... } 不会自动绑定;传递 onClick={this.addOne} 只取出函数,未来调用不能依赖原接收者。可在constructor中先 super(props),再 this.addOne = this.addOne.bind(this)。两种写法都可维护,不必为了统一风格重写所有方法。

把上例addOne临时改成普通方法并点击,是观察this错误的反例;恢复箭头字段后继续。super(props) 建立父类初始化,必须先于constructor内使用this。state字段等价于实例初始化的一部分,但constructor/state初始化不能启动订阅或计时器,因为这个实例不一定真正提交到页面。

实例还可保存timer、连接句柄等不参与UI的字段。修改它们不会发起渲染,正如函数组件里改变ref.current不发起渲染。下一节将用它们记录资源的所有权。

updater还接收第二个参数props:(previous, props) => ({ count: previous.count + props.step })。它适合让下一状态同时取决于队列前一步和当前输入,而不是把props复制成另一份state。

练习:按父组件步长连续累加 ​

给Counter增加 step prop,约定为1到5的整数,父组件已用state初始传2,按钮可在2与3之间切换。新增按钮“一次增加三步”,使用三个updater,每个读取updater的第二个参数props,保证从0到6、再到12,note不丢失。随后点击页面上的“切换步长”把prop变为3,下一次从12到21。不要通过编辑源码改常量来执行这一步:开发热更新可能重建类实例,造成计数重置;本题要验证同一类型、同一key下的运行时prop更新。不要把step复制进state,不修改原状态。

查看实现与观察。接着阅读资源生命周期。

用心记录每一步成长