前端知库

React 类组件有哪些生命周期?在函数组件里怎样对应?

挂载时依次调用 constructor、getDerivedStateFromProps、render、componentDidMount;更新时调用 getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate;卸载时调用 componentWillUnmount。函数组件里,componentDidMount、componentDidUpdate 和 componentWillUnmount 组合起来大致对应 useEffect,shouldComponentUpdate 对应 memo,错误边界目前仍只能用类组件写。

参考答案

简答

类组件从出现到消失分三个阶段。挂载阶段,React 先调用 constructor 初始化 state,再调用静态方法 getDerivedStateFromProps,然后 render 算出界面,DOM 插入页面后调用 componentDidMount,这里适合发请求、订阅事件、操作 DOM。

更新阶段,props 或 state 变化时,先调用 getDerivedStateFromProps,再问 shouldComponentUpdate 要不要重新渲染,返回 false 就跳过。需要渲染就调用 render,在 DOM 真正更新之前调用 getSnapshotBeforeUpdate,可以读取更新前的 DOM 信息,比如滚动位置,它的返回值会传给接下来的 componentDidUpdate。卸载阶段只有 componentWillUnmount,在这里取消订阅、清除定时器。

函数组件没有这些方法。React 文档的说法是,多数情况下 componentDidMount、componentDidUpdate、componentWillUnmount 三者组合起来,等价于在函数组件里调用 useEffect,需要在浏览器绘制前执行时用 useLayoutEffect。shouldComponentUpdate 的作用对应 memo。

要注意两点。componentWillMount、componentWillReceiveProps、componentWillUpdate 已被改名为带 UNSAFE_ 前缀的版本,旧名字已废弃,新代码不要用。另外,捕获子组件渲染错误的 getDerivedStateFromError 和 componentDidCatch 在函数组件里还没有对应的写法,错误边界目前仍然要写成类组件。

一个完整的类组件

下面的聊天室组件展示挂载、更新、卸载三个阶段该做的事:

JSX
class ChatRoom extends Component {
  state = { messages: [] };

  componentDidMount() {
    this.connection = connect(this.props.roomId);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.roomId !== this.props.roomId) {
      this.connection.disconnect();
      this.connection = connect(this.props.roomId);
    }
  }

  componentWillUnmount() {
    this.connection.disconnect();
  }

  render() {
    return <MessageList messages={this.state.messages} />;
  }
}

为什么三个方法都要写?componentDidMount 建立了连接,如果 roomId 后来变了,不在 componentDidUpdate 里重新连接,组件就一直连着旧房间;组件卸载时不在 componentWillUnmount 里断开,连接就会泄漏。React 文档特别提醒,实现了 componentDidMount,通常也要实现另外两个。

开发环境的严格模式下,React 会先调用 componentDidMount,立刻调用 componentWillUnmount,再调用一次 componentDidMount,专门检查清理逻辑有没有写全。

换成函数组件

JSX
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = connect(roomId);
    return () => connection.disconnect();
  }, [roomId]);

  return <MessageList messages={messages} />;
}

三个生命周期方法被一个 useEffect 代替了。Effect 第一次执行相当于 componentDidMount;roomId 变化时,React 先执行上一次返回的清理函数,再用新的 roomId 执行一次,相当于 componentDidUpdate 里先断开再重连;组件卸载时执行清理函数,相当于 componentWillUnmount。

两种写法的思路不同。类组件按“什么时候”组织代码,函数组件按“要同步什么”组织代码,连接房间的逻辑都放在一个 Effect 里,不容易漏掉某个阶段。依赖数组怎样写,见《useEffect 的依赖数组和清理函数应该怎么写?》。

常见追问

在 componentDidMount 里调用 setState 会怎样?

会多触发一次渲染,但这次渲染发生在浏览器更新屏幕之前,用户看不到中间状态。React 文档建议尽量避免,初始 state 应该在 constructor 里设置;只有像弹窗、提示框这种要先测量 DOM 尺寸再决定位置的情况才需要这样做。

getDerivedStateFromProps 适合什么时候用?

很少需要。它在每次渲染前都会调用,用来在 props 变化时更新 state,比如 userID 变了就重置表单。多数场景有更简单的做法,比如给组件换一个 key 让它整体重置。

错误边界为什么只能用类组件写?

因为 getDerivedStateFromError 和 componentDidCatch 还没有对应的 Hook。不想写类组件时,可以写一个通用的 ErrorBoundary 类组件全局复用,或者用 react-error-boundary 这个库,见《Error Boundary 和 Suspense 分别处理什么?》。