Skip to content

挂载、更新与卸载:资源归谁所有 ​

“挂载时订阅,卸载时取消”只覆盖了两端。如果组件一直存在,但它从杭州频道切换到了北京频道,旧订阅也必须退出。把生命周期当作一组按时间执行的方法名,容易忘记这个中间过程;从“当前props需要哪一个外部连接”出发,才知道更新阶段要比较什么。

一个不需要网络的频道 ​

下面的事件通道用Map保存频道对应的回调集合。subscribe返回取消函数,这是本实验的资源协议;publish只向当前订阅者发送消息;count只用于观察。它们都是普通JavaScript,不是React API。完整替换 src/App.jsx:

jsx
import { Component, useState } from 'react'

export const bus = {
  channels: new Map(),
  subscribe(room, listener) {
    let listeners = this.channels.get(room)
    if (!listeners) this.channels.set(room, listeners = new Set())
    listeners.add(listener)
    return () => {
      listeners.delete(listener)
      if (!listeners.size) this.channels.delete(room)
    }
  },
  publish(room, text) {
    this.channels.get(room)?.forEach(listener => listener(text))
  },
  count(room) { return this.channels.get(room)?.size ?? 0 }
}

export class Room extends Component {
  state = { message: '尚无消息' }
  unsubscribe = null

  connect() {
    this.unsubscribe = bus.subscribe(this.props.room, message => {
      this.setState({ message })
    })
  }
  disconnect() {
    this.unsubscribe?.()
    this.unsubscribe = null
  }
  componentDidMount() { this.connect() }
  componentDidUpdate(previousProps) {
    if (previousProps.room !== this.props.room) {
      this.disconnect()
      this.connect()
      this.setState({ message: '尚无消息' })
    }
  }
  componentWillUnmount() { this.disconnect() }
  render() {
    return <p role="status">{this.props.room}:{this.state.message}</p>
  }
}

export default function App() {
  const [room, setRoom] = useState('杭州')
  const [shown, setShown] = useState(true)
  const [counts, setCounts] = useState('尚未检查')
  return <main>
    <button onClick={() => setRoom(r => r === '杭州' ? '北京' : '杭州')}>切换频道</button>
    <button onClick={() => setShown(v => !v)}>显示或卸载</button>
    <button onClick={() => bus.publish('杭州', '杭州消息')}>发布杭州</button>
    <button onClick={() => bus.publish('北京', '北京消息')}>发布北京</button>
    <button onClick={() => setCounts(`杭州${bus.count('杭州')},北京${bus.count('北京')}`)}>检查订阅</button>
    <output>{counts}</output>
    {shown && <Room room={room} />}
  </main>
}

首次稳定显示后检查,杭州1、北京0;切到北京再检查,杭州0、北京1。此时发布杭州不应修改北京的status,发布北京才显示北京消息。卸载后两边都是0;再次显示仍只有北京一个订阅。StrictMode开发检查可能出现mount→unmount→mount,因此应观察最终存活数,不要求connect日志只出现一次。

三个阶段各知道什么 ​

componentDidMount在初次DOM提交后启动外部同步,render只描述UI。componentDidUpdate拿到previousProps/previousState,而this.props/this.state已是这次提交的值,因此可以识别连接参数变化。componentWillUnmount撤销当前资源;React不会替你取消普通事件总线、timer或socket。

上例在didUpdate里setState会导致一次后续更新,但不形成死循环:只有room变化才进入分支,后一次提交的previousProps.room已等于当前room。若删除条件直接setState,每次更新又生成下次更新,这才是循环。将message清空是这个实验的产品合同,不是每次prop变化都必须复制一份state;若整段局部状态都应重置,也可使用key更明确地重建实例。

disconnect先执行旧取消函数,再把句柄清空。取消函数捕获旧listeners集合,所以切换props后仍能准确退出旧频道。反过来,在卸载时根据“最新room”重新拼一个退订请求,未必能对应原来订阅的对象。这也是保存返回句柄优于猜测如何撤销的原因。

请求失败与晚到结果还要多一层合同 ​

对fetch一类异步资源,可保存AbortController,并在切换/卸载abort;同时用请求序号或取消标记判断旧结果是否仍有写入资格。取消底层网络和阻止旧响应写state是两个责任;服务端已经接受请求时,abort不会回滚它的业务副作用。第四章的清理与竞态已给出可控制完成顺序,这一协议迁到类中仍然适用。

不要把资源创建放进constructor、render、getDerivedStateFromProps或UNSAFE_生命周期。渲染阶段可能重做或被舍弃,并不保证随后发生一次对应的提交和卸载。旧代码的componentWillReceiveProps里既同步state又发请求时,先拆开派生计算和连接责任,再决定保留哪段。

类可以声明 static defaultProps = { active: true },为调用方省略或传undefined的prop提供默认值;显式false仍保留。static字段属于组件类,不是每个实例的state,默认值不会创建第二个active数据源。React19仍支持类defaultProps;这与函数参数默认值是不同的写法。

练习:暂停也要撤销订阅 ​

为Room增加布尔prop active,默认true。父组件增加暂停/恢复按钮。active为false时当前频道没有订阅,消息不会更新;在暂停期间切频道,恢复后只订阅新频道;卸载永远留下0个订阅。继续使用同一实例,不用key绕过更新逻辑。比较room与active两个连接输入,并验证“暂停→切频道→恢复→发布两边→卸载”的连续顺序。

查看改动与观察。下一节在更新阶段继续观察跳过渲染与提交前快照。

用心记录每一步成长