Menu

React 组件生命周期:挂载、更新与卸载

每个 React 组件都会挂载(出现)、更新(再次渲染)和卸载(被移除)。学习每个阶段如何对应到 useEffect,类组件的生命周期方法及其对应的 Hook 写法,以及渲染阶段和提交阶段。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

React 组件生命周期有三个阶段:挂载(组件第一次出现在屏幕上)、更新(因为 state、props 或 context 改变而再次渲染)和卸载(组件被移除)。在函数组件中,你用 useEffect 来响应这些阶段:依赖为 [] 的 effect 在挂载后运行,带依赖的 effect 在依赖发生变化的更新后运行,而 effect 返回的函数在卸载时运行。

预览下方的控制台会实时显示每个阶段。

控制台一开始显示 render, count = 0 和 mounted。每次点击 Count 都会记录一次渲染和一次更新。点击 Unmount 会记录 unmounted;再点击 Mount 会从 count = 0 重新开始,因为 state 已经随组件一起被丢弃了。

挂载

当 React 第一次在组件树的某个位置渲染一个组件时,这个组件就挂载了。React 调用这个函数,根据它返回的 JSX 创建 DOM 节点并插入页面,让浏览器完成绘制,然后运行组件的 effect。

useEffect(fn, []) 是启动那些只要组件还在屏幕上就应该一直存在的东西的地方:订阅、定时器、连接、首次数据请求。它在绘制之后运行,所以永远不会拖慢第一帧。

更新

在以下情况下,组件会更新(再次渲染):

  • 它自己的 state 改变了,
  • 它的父组件再次渲染(默认情况下,无论 props 是否改变,每个子组件也都会渲染),
  • 它读取的某个 context 改变了。

每次更新之后,React 会运行依赖发生变化的那些 effect。在再次运行某个 effect 之前,它会先运行这个 effect 上一次的清理函数,所以旧的订阅会在新的订阅建立之前关闭。渲染一页更详细地介绍了哪些情况会触发渲染。

卸载

当父组件不再渲染某个组件(就像上面的 {show && <Counter />})、它的 key 改变,或者根节点本身被卸载时,这个组件就会卸载。React 会移除它的 DOM 节点,运行所有 effect 的清理函数,并丢弃它的 state。如果这个组件之后再次出现,它就是一个全新的实例。

父组件与子组件

React 自上而下渲染,却自下而上运行 effect。当父组件和子组件一起挂载时,子组件的 effect 先运行,所以父组件的 effect 可以放心地认为子组件已经完全准备好了。

挂载时依次记录 render Parent、render Child、Child effect、Parent effect。点击 Remove,清理函数会按相反的顺序运行:先 Parent cleanup,再 Child cleanup。

渲染阶段与提交阶段

每次更新都会经历两个阶段:

  1. 渲染阶段。React 调用你的组件来计算新的 JSX,并和上一次的结果进行比较。此时屏幕上还没有任何变化。React 可能会暂停这部分工作,在 StrictMode 下运行两次,或者把它丢弃,这就是为什么渲染必须是纯的:不发请求,不建订阅,不修改外部变量。
  2. 提交阶段。React 把差异应用到 DOM 上,绑定 ref,并运行 useLayoutEffect 回调,这一切都发生在浏览器绘制之前。useEffect 回调在那之后运行,通常是在浏览器完成绘制之后。如果更新是由点击或其他离散输入引起的,React 会改为在 layout effect 之后立即运行它们,让结果在下一个事件之前就准备好。

每次点击都会按相同的顺序记录这三行。这些 effect 没有依赖数组,所以每次渲染后都会运行。给两者都加上 [] 作为第二个参数,它们就只会在第一次渲染后运行。

类组件生命周期方法及其对应的 Hook 写法

类组件把生命周期写成有固定名称的方法。你会在较老的代码以及错误边界中遇到它们,错误边界至今仍然需要用类来写。

类方法运行时机函数组件中的对应写法
constructor一次,在首次渲染之前useState(initial)、useRef
static getDerivedStateFromProps每次渲染之前在渲染期间计算该值,或者用 key 重置
render每次渲染函数体
componentDidMount首次渲染的结果进入 DOM 之后useEffect(fn, [])
shouldComponentUpdate更新之前,可以跳过更新React.memo(它返回的是相反的答案:“props 是否相等”)
getSnapshotBeforeUpdate紧接在 DOM 变化之前没有直接对应的 Hook;最接近的是 useLayoutEffect 配合 ref
componentDidUpdate每次更新之后useEffect(fn, [deps])(挂载后也会运行)
componentWillUnmount移除之前useEffect 返回的清理函数
getDerivedStateFromError、componentDidCatch子组件抛出错误时没有:编写一个错误边界类

下面这个类会在每个方法运行时记录日志:

挂载时依次记录 constructor、getDerivedStateFromProps、render、componentDidMount。点击一次会记录 getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate。让 shouldComponentUpdate 返回 false,点击就不再更新按钮了:state 改变了,但 React 跳过了渲染。类组件一页展示了如何把这样的类改写成函数组件。

用同步的思路思考,而不是生命周期

上面的对照表有助于阅读老代码,但它可能会让你在写 effect 时照搬类组件的习惯,比如写一个大的“did mount” effect 和一个大的“did update” effect。更好的理解方式是把 effect 读作“让这个外部事物与这些值保持同步”:连接到 roomId,当 roomId 改变时,断开连接并重新连接。每同步一样东西就写一个 effect,把它用到的值列为依赖,然后让 React 决定何时运行它。

常见问题

React 组件生命周期有哪些阶段?

挂载(React 创建组件并把它的输出显示到屏幕上)、更新(因为 state、props 或 context 改变而再次渲染)和卸载(组件被移除并执行清理)。

componentDidMount 对应的 Hook 写法是什么?

useEffect(() => { ... }, [])。空的依赖数组让它只运行一次,在首次渲染的结果显示到屏幕上之后运行。它返回的函数相当于 componentWillUnmount。

componentDidUpdate 对应的 Hook 写法是什么?

在依赖数组中放入你关心的值的 useEffect,例如 [userId]。与 componentDidUpdate 不同,它在首次渲染之后也会运行,所以同时涵盖了挂载和更新。

父组件和子组件的 effect 按什么顺序运行?

React 从父组件往下渲染,但从子组件往上运行 effect:子组件的 effect 先于父组件的运行。卸载时,父组件的清理函数先于子组件的运行。

什么是渲染阶段和提交阶段?

在渲染阶段,React 调用你的组件,算出屏幕应该是什么样子;这部分工作可以暂停,也可以丢弃。在提交阶段,React 把变化应用到 DOM 上并运行 layout effect;普通的 effect 在那之后运行,通常是在浏览器完成绘制之后。

Coddy 编程语言插图

和 Coddy 一起学编程

开始