InheritedWidget:.of(context) 的 O(1) 魔法
你每天写 Theme.of(context)、MediaQuery.of(context),它们背后是同一个机制——InheritedWidget。这是 Flutter 处理「跨层共享数据」的原生答案,也是所有状态管理库(Provider、Riverpod)的地基。而它有一个让人惊讶的性质:查找是 O(1) 的,和树多深无关。这一章用一台真引擎给你看清它。
它解决的问题:别再一层层往下传了
你有一个主题色,深埋在树里第 8 层的一个按钮要用。用构造函数传?那得让中间 7 层每一层都加一个自己根本不关心的 themeColor 参数,一路往下递——这就是臭名昭著的 prop drilling。
InheritedWidget 让你把一个值「挂」在树的某个高处,它下面的任何子孙,无论隔多少层,都能直接伸手拿到,中间层完全无感。这就是「就近取值」——和 Compose 的 CompositionLocal 是同一个东西、同一个用途。
亲手跑:真的查找引擎
下面这台构建了一棵真的树,Theme(一个 InheritedWidget)挂在高处,PriceText 埋在第 5 层。点任意一行,看它调 Theme.of(context) 时发生了什么;再点「切换主题」,看谁被标脏。
两个数字是这一章的全部重点:
Theme.of(context):1 步。不管这个节点在第 5 层还是第 50 层,一次哈希查表就拿到。
context.findAncestorWidgetOfExactType<Theme>():4 层。老老实实一层层往上爬,深度是几就爬几层。
这就是官方反复警告「findAncestor… 很贵,别在 build 里用」的原因——它是 O(树深度),而 .of() 是 O(1)。
O(1) 是怎么做到的:每个 Element 继承一张哈希表
秘密在第 6 章那张 Element 字段表里的一行:_inheritedWidgets。机制是这样的:
- 每个 Element 挂载时,会做一步
_updateInheritance:把父亲那张「类型 → InheritedElement」的哈希表抄一份给自己。 - 如果它自己就是个 InheritedWidget(比如
Theme),就在这份表里把自己也加进去。 - 于是树里每个 Element 手上,都有一张「从我往上,所有祖先 InheritedWidget」的现成索引。
所以 Theme.of(context) 做的就是 context._inheritedWidgets[Theme]——一次哈希查表。树多深都无所谓,因为表在挂载时就一路继承下来了。空间换时间,换得很值。
那个「.of() 有时候报错」的经典坑,破了
第 6 章埋过一个伏笔:Scaffold.of(context) 有时报 No Scaffold widget found。现在你能自己解释了:
// ✗ 报错:这个 context 在 Scaffold 上面,它的哈希表里没有 Scaffold
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(...); // context 是谁的?
},
child: const Text('弹提示'),
),
);
}
}
问题在:这个 context 是 MyPage 这个节点的,它在 Scaffold 的上面——它继承的哈希表里当然没有下面才出现的 Scaffold。解法是拿一个在 Scaffold 下面的 context:用 Builder 包一层,或把按钮抽成子 Widget(它的 build 里的 context 就在 Scaffold 之下了)。
X.of(context) 只能往上找,找的是「创建这个 context 的节点」的祖先。如果你要的东西在这个 context 的下面或旁边,就换一个位置更靠下的 context(Builder 是最轻的办法)。ScaffoldMessenger(比 Scaffold.of 更靠上)就是为了缓解这个坑而设计的。
另一半魔法:切换时只重建「订阅了的人」
点引擎里的「切换主题」,看被标脏的是谁——只有调用过 Theme.of(context) 的那几个节点,其它一个都不动。这不是巧合,是 InheritedWidget 的第二个核心机制:依赖登记。
当你调 Theme.of(context)(内部是 dependOnInheritedWidgetOfExactType),除了查表返回值,它还悄悄干了一件事:把「我」登记进这个 Theme 的依赖名单。于是当 Theme 的值变化时:
- 框架调
updateShouldNotify(old)问它:「值真的变了吗,要不要通知?」 - 返回 true → 调
notifyClients,把依赖名单里的每一个 Element 标脏(下一帧重建)。 - 没登记过依赖的节点(只是路过、从没调过
.of())—— 一个都不碰。
再点引擎里的「再切一次(值不变)」:0 个节点被标脏。因为 updateShouldNotify 返回了 false——值没变,通知谁都是浪费。
如果你哪天自己继承 InheritedWidget,这个方法写错(永远返回 true)会让所有依赖者每次都重建,哪怕值根本没变:
class ThemeScope extends InheritedWidget {
const ThemeScope({required this.color, required super.child});
final Color color;
static ThemeScope of(BuildContext c) =>
c.dependOnInheritedWidgetOfExactType<ThemeScope>()!;
@override
bool updateShouldNotify(ThemeScope old) => color != old.color; // ✓ 只在真变时通知
// ✗ 写成 => true,就是性能灾难
}
好消息是:日常你几乎不用手写这个类——Provider、Riverpod 都替你处理好了 updateShouldNotify。但理解它,你才知道那些库到底在你背后省了什么。
它是所有状态管理库的地基
这句话值得你记住,它能让下一章变得简单:
Provider = InheritedWidget(挂值、管依赖登记、管 updateShouldNotify)+ 一点点让 API 好用的封装。context.watch<T>() 就是 dependOn…(订阅),context.read<T>() 就是「查一下但不登记依赖」(不订阅)。
连 Riverpod——虽然它把 provider 定义搬到了树外——最终把值送到 Widget 的那一步,靠的还是一个叫 ProviderScope 的 InheritedWidget。所以第 16 章那些库,你都可以用这一章的机制去理解它们「重建了谁」。
你天天在用的那些「.of」,都是它
理解了这一章,Flutter 里一大批看起来无关的 API 会突然归为一类——它们全是 InheritedWidget,全走同一台机器:
| API | 拿到什么 | 变化时会重建订阅者吗 |
|---|---|---|
Theme.of(context) | 颜色、字体、主题 | 会(切深色模式时) |
MediaQuery.of(context) | 屏幕尺寸、键盘高度、字体缩放 | 会(旋转、键盘弹出时) |
Directionality.of(context) | 文字方向(LTR/RTL) | 会 |
Localizations.of(context) | 当前语言的文案 | 会(切语言时) |
DefaultTextStyle.of(context) | 默认文字样式 | 会 |
Navigator.of(context) / ScaffoldMessenger.of | 导航器 / 消息器(拿来调方法) | 一般不订阅它的重建 |
这解释了一个你可能遇到过的现象:为什么改一下系统字体大小、或者旋转屏幕,某些页面会重新布局——因为它们 MediaQuery.of(context) 了,被登记成了依赖,尺寸一变就被通知重建。这不是 bug,是这套机制在正常工作。
MediaQuery.of(context) 会订阅整个 MediaQueryData,所以键盘一弹出(viewInsets 变)、字体缩放一调(textScaler 变),你这个 Widget 就重建——哪怕你只关心屏幕宽度。较新的 Flutter 给了精确订阅的方法,只订阅你要的那个字段:
// ✗ 键盘弹出也会让这里重建 final w = MediaQuery.of(context).size.width; // ✓ 只订阅 size,键盘变化不打扰 final w = MediaQuery.sizeOf(context).width; final kb = MediaQuery.viewInsetsOf(context).bottom; // 只订阅键盘高度
这就是「把 .of() 调用点收窄」在框架 API 层面的体现——第 14 章的手艺,官方替你做进了 sizeOf/viewInsetsOf 这些细粒度方法里。能用 xxxOf 就别用 .of().xxx。
| Compose | Flutter |
|---|---|
staticCompositionLocalOf { } / compositionLocalOf | InheritedWidget 子类 |
CompositionLocalProvider(x provides v) { } | 把 InheritedWidget 放到树上 |
LocalX.current | X.of(context) |
读了 .current 的作用域自动重组 | 调了 .of() 的 Element 被登记、被标脏 |
机制几乎一模一样——都是「就近提供、按需订阅、变了只通知订阅者」。唯一的粒度差别:Compose 的重组作用域可以细到一个可组合函数内部;Flutter 标脏的单位是调用了 .of() 的那个 Element 整棵子树。所以在 Flutter 里,你会把 .of() 调用点放到尽可能小、尽可能低的 Widget 里(回到第 14 章的手艺),来把重建范围压小。你在 Compose 里对 CompositionLocal 的全部直觉,这里都成立。
这一章的一句话
InheritedWidget 是 Flutter 版的 CompositionLocal:每个 Element 挂载时继承一张祖先索引哈希表,所以 .of(context) 是 O(1) 查表而非爬树;它还登记依赖、只在 updateShouldNotify 为真时重建订阅者——而这正是 Provider 和 Riverpod 底下的同一台机器。
地基铺好了。下一章我们爬到地面上——面对那个每个 Flutter 新人都要问、答案又一直在变的问题:Provider、Riverpod、Bloc,我这个学校 App 到底该用哪个?我会给你一个 2026 年的明确答案,并用引擎让你看清它们「重建了谁」的差别。