5.4 组件生命周期与副作用管理


5.4 组件生命周期与副作用管理

本节摘要:组件从创建到销毁有一整套生命周期:挂载、更新、卸载。RN 用 useEffect 等 Hooks 或类组件生命周期方法,Flutter 用 initState、didUpdateWidget、dispose 等方法。本节讲清两个框架的生命周期时间线与副作用管理——网络请求、订阅、清理都必须在正确的时机执行,否则就会出现内存泄漏或数据错乱。核心准则是"创建与清理的对称性"。

你能学到什么

阅读完本节,你应当能够:

  1. 说出 RN 类组件与函数组件的生命周期对应关系。
  2. 用 useEffect 管理 RN 函数组件的副作用。
  3. 说出 Flutter State 的主要生命周期方法及用途。
  4. 判断副作用(请求、订阅、清理)该在哪个时机执行。

一、问题与直觉

先看一个真实事故的轮廓:一个聊天应用,打开会话页,网络请求开始加载历史消息。用户很快退出了页面,但请求还没返回。几秒后数据返回,代码尝试更新一个已经卸载的页面——要么报错,要么数据白白流失,还可能在后台默默占用资源。这类问题的根源不是"请求写错了",而是"副作用没有在正确的生命周期时机处理"。

生命周期管理就是回答这个问题:组件什么时候创建、什么时候更新、什么时候销毁,每个阶段该做什么、不该做什么。它决定了应用是否健壮——资源泄漏、数据错乱、后台浪费,大多源于生命周期时机错位。

RN 与 Flutter 的生命周期概念同源,都是"挂载 → 更新 → 卸载"三阶段。掌握一张时间线,两套框架只是方法名不同。

二、核心原理

2.1 RN 生命周期:类组件与 Hooks 的对应

RN 的类组件有一套经典生命周期:

挂载阶段:constructor 初始化 → render 渲染 → componentDidMount 执行副作用(请求、订阅、定时器)。

更新阶段:shouldComponentUpdate 决定是否重渲 → render → componentDidUpdate 响应更新(重新请求、更新订阅)。

卸载阶段:componentWillUnmount 清理(取消请求、清定时器、取消订阅)。

2.1.1 类组件生命周期的主要方法

把关键方法逐一讲清,理解每个方法"什么时候调用、该做什么":

constructor。组件挂载前调用,初始化 state、绑定事件。不要在 constructor 里执行副作用(如发请求)——组件可能挂载前就被卸载,请求白发了。必须调用 super(props)。

render。唯一必须实现的方法,返回界面描述。它是纯函数,不改状态、不执行副作用。把 render 当成"纯计算"是正确姿势。

componentDidMount。挂载完成后立即调用,是执行副作用的标准位置:发请求、订阅事件、设定时器。这是"副作用窗口"的第一个入口。

componentDidUpdate。更新完成后调用。适合在 props 或 state 变化后重新获取数据、更新订阅。注意:在这里 setState 必须放在条件判断里,否则触发更新又触发 componentDidUpdate 又 setState,形成无限循环。

componentWillUnmount。卸载前调用,清理 componentDidMount 里创建的资源:取消请求、清定时器、取消订阅。不要在这里 setState——组件马上销毁,更新无意义。

2.1.2 useEffect 的三种依赖形态

函数组件用 useEffect 替代以上所有生命周期,通过依赖数组控制行为:

// 形态一:空依赖,挂载时执行一次,卸载时清理 useEffect(() => { doOnce(); return cleanup; }, []); // 形态二:有依赖,挂载与依赖变化时执行,变化前先清理 useEffect(() => { loadBy(roomId); return cancelBy; }, [roomId]); // 形态三:无依赖数组,每次渲染后都执行(要谨慎) useEffect(() => { console.log('每次渲染后'); });

形态三最容易被误用。没有依赖数组的 useEffect 每次渲染都执行,通常用于"每次渲染后同步外部系统",但大部分场景应该显式声明依赖。依赖数组的正确写法是"列出所有用到的外部变量"——漏掉依赖可能导致闭包捕获过期值,这也是 React 社区最经典的坑之一。

现代 RN 推荐函数组件加 Hooks,useEffect 是生命周期的"统一入口":

import React, { useState, useEffect } from 'react'; function ChatScreen({ roomId }) { const [messages, setMessages] = useState([]); useEffect(() => { // 挂载与依赖变化时执行 const fetchMessages = async () => { const data = await loadHistory(roomId); setMessages(data); }; fetchMessages(); // 返回的清理函数:卸载与依赖再次变化前执行 return () => { cancelHistoryRequest(); }; }, [roomId]); return <FlatList data={messages} renderItem={renderMsg} />; }

useEffect 的第二个参数是依赖数组:空数组表示只在挂载时执行一次;含变量表示变量变化时重新执行;返回的清理函数在卸载时执行。

2.2 Flutter 生命周期:State 的时机表

Flutter 的生命周期落在 State 对象上:

创建:initState 一次性初始化(建控制器、订阅数据流)。

依赖变化:didChangeDependencies 读取 InheritedWidget 数据。

更新:didUpdateWidget 响应父组件传来的新配置。

销毁:dispose 清理资源(取消订阅、释放控制器)。

class _ChatState extends State<ChatScreen> { @override void initState() { super.initState(); _loadHistory(); // 挂载时加载 } @override void dispose() { _cancelHistory(); // 销毁时清理 super.dispose(); } @override Widget build(BuildContext context) { return ListView(...); } }

对应关系一目了然:Flutter 的 initState 对应 RN 的挂载副作用,Flutter 的 dispose 对应 RN 的清理函数。两个框架都遵循"创建时初始化、销毁时清理"的对称原则。

2.2.1 Flutter 生命周期方法的调用顺序

Flutter 的 State 生命周期有一个固定顺序,掌握它就能预判"什么时候该做什么":

initState ↓ didChangeDependencies ↓ build ↓ (依赖变化时)didChangeDependencies → build ↓ (父组件更新时)didUpdateWidget → build ↓ (移除时)deactivate ↓ (销毁时)dispose

这条时间线的价值在于预判。写代码时心里默念这个顺序:initState 只跑一次、build 会被反复调用、dispose 只跑一次。据此决定每个逻辑的放置位置——一次性初始化放 initState,反复重建的放 build,资源清理放 dispose。顺序错了,bug 就来了:把初始化放 build 里会重复执行,把清理放 build 里会在不该清理时清理。

2.2.2 didUpdateWidget 的典型用法

didUpdateWidget 在父组件传入新 Widget 时调用,是"响应配置变化"的窗口。典型场景:列表项组件收到了新的数据项,需要重新同步内部状态或重新请求。

@override void didUpdateWidget(covariant OldWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.roomId != widget.roomId) { _loadMessages(widget.roomId); // 房间变化,重新加载 } }

注意对比新旧值再决定要不要动作——这是避免不必要工作的关键。它对应 RN 的 componentDidUpdate,用法几乎一样:比较 prevProps 与当前 props,有变化才重新获取。

2.3 为什么时机错了会出事

用聊天例子讲清时机错误的后果:

请求在挂载后执行,数据回来更新界面——正确。

请求在组件已卸载后返回并更新——要么报错、要么白费资源。解法:卸载时取消请求或忽略更新。

订阅在挂载时建立但不取消——页面关了,订阅还在,持续消耗资源。解法:dispose 或清理函数里取消。

定时器不清理——页面关了还每秒执行一次,泄漏累积。解法:卸载时清除。

这些事故的共性是:"开始"与"结束"不对称。开始做的事(请求、订阅、定时器)没有在结束时对称地清理。生命周期管理,本质就是保证这种对称性。

三、工程实践要点

3.1 生命周期对照速查

阶段 React Native(Hooks) Flutter(State)
挂载初始化 useEffect 空依赖 initState
依赖变化 useEffect 依赖数组 didChangeDependencies / didUpdateWidget
卸载清理 useEffect 返回的清理函数 dispose
渲染 render / 组件函数体 build

3.2 副作用管理的黄金法则

法则一:请求的发起与取消要成对。发起请求的地方,要能取消请求。

法则二:订阅与退订要成对。订阅建立的地方,要有对应的取消。

法则三:定时器与清除要成对。setTimeout/setInterval 建立,对应时机清除。

法则四:清理时机提前考虑。写"开始"时就想好"何时结束",而不是等泄漏发生。

⚠️ 常见坑:在渲染函数里发网络请求。RN 的组件函数体与 Flutter 的 build 都可能被频繁调用,把请求放里面会造成重复请求风暴。请求必须放在 useEffect(RN)或 initState(Flutter)等正确的生命周期时机。
💡 关键直觉:生命周期的对称性是判断代码是否健壮的标尺——凡是"创建了什么"的地方,都要问一句"在哪里销毁它"。对称性成立,资源就安全。

3.3 动手验证:给待办页面加副作用

回到待办应用,做两个练习:在挂载时加载一组模拟数据(体验挂载时机);切换主题时重新读取共享数据(体验依赖变化时机)。然后故意"忘记清理",观察内存与行为的异常,再补上清理。这套"先做错、再改正"的练习,能最快建立对生命周期时机的敏感度。

FAQ:生命周期的高频问题

问:useEffect 里的清理函数什么时候执行? 两个时机:组件卸载时执行一次(相当于 componentWillUnmount);依赖变化重新执行副作用之前,先执行上一次的清理。理解"先清理、再执行"这个循环,useEffect 的行为就完全可预测了。

问:Flutter 的 deactivate 和 dispose 有什么区别? deactivate 是"可能离开渲染树",未来可能被重新插回(比如列表滚动时项移出可视区);dispose 是"永久移除",不会再回来。清理资源的动作应该放 dispose,deactivate 只做临时的轻量处理。把清理放错到 deactivate 可能导致重复或过早清理。

问:数据请求放 initState 还是 didChangeDependencies? 多数场景 initState 够用——它只执行一次,适合一次性数据加载。但如果你需要读取 InheritedWidget 提供的依赖数据,必须用 didChangeDependencies,因为 initState 里还不能安全访问继承的 Widget。判断标准:请求依赖共享数据吗?依赖就用后者,否则用前者。

问:为什么强调不要在渲染里发请求? RN 的组件函数体与 Flutter 的 build 都可能因父组件重建、状态变化、主题切换等被频繁调用。请求放里面等于"每次渲染都请求",轻则浪费带宽,重则造成请求风暴打垮后端。请求永远要放进正确的生命周期时机,这是新手最容易犯也最容易被忽略的错误。

3.5 关于副作用的三个原则

把副作用的时机管理浓缩成三个原则,便于记忆与应用:请求跟随生命周期——发起请求的时机与组件挂载对应,取消请求与组件卸载对应;订阅必有退订——任何订阅(事件、数据流)建立时就想好退订位置;初始化一次——只在 initState 或 mount 阶段做一次性初始化,别在 build 里重复执行。这三条原则覆盖了生命周期管理的绝大多数场景。写代码时对照检查,比死记生命周期方法名更不容易出错——原则是"为什么",方法名只是"怎么做"的载体。

3.6 一个常见误区的澄清

"生命周期越多方法越高级"是个常见误区。实际相反:现代 React 用 useEffect 把生命周期统一了,Flutter 的状态管理也会用工具自动处理部分生命周期。代码里生命周期方法用得越少、越集中在必要时,通常说明架构越简洁。判断代码健壮与否的标准不是"生命周期方法数量",而是"每个副作用是否在正确时机执行且成对清理"。把注意力从"方法个数"转移到"时机正确性",才是真正的工程思维。

要点速记

  • 三阶段:挂载、更新、卸载,RN 与 Flutter 概念同源。
  • RN 类组件:componentDidMount、componentDidUpdate、componentWillUnmount 三段式。
  • RN Hooks:useEffect 统一管理副作用,依赖数组控制执行时机,返回清理函数。
  • useEffect 三形态:空依赖一次性、有依赖随变、无依赖每次渲染(慎用)。
  • Flutter State:initState 初始化、didChangeDependencies 读共享数据、dispose 清理。
  • 时间线预判:记住 initState 一次、build 反复、dispose 一次,逻辑放对位置。
  • 对称原则:请求与取消、订阅与退订、定时器与清除都要成对。
  • 常见事故:卸载后更新状态、订阅不取消、定时器不清理,都是不对称所致。

生命周期与副作用管理完毕,第 5 章收官。下一章进入常用功能与工程实践——导航、网络、存储、手势、原生能力与调试,把学到的一切组装成完整应用。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U