网易首页 > 网易号 > 正文 申请入驻

Flutter 新一代状态管理框架 signals ,它究竟具备什么魔法和优势

0
分享至

在上一篇《Riverpod 的注解模和发展方向》里就有很多人提到 signals ,对比 riverpod 部分人更喜欢 signals 的 “简单”和“直接”,那 signals 真的简单吗?再加上前段时间 signals 和 riverpod 的性能对比风波,也让大家更加关注 signals ,那它究竟有什么「魔力」?

signals.dart 有多“简单”?大概就是它的状态管理可以“简单”到甚至和 Flutter 没有关系,如下代码所示:

  • 通过signal创建一个信号对象

  • 通过computed可以合并多个signal

  • 通过effect可以监听响应数据变化

import 'package:signals/signals.dart'; final name = signal("N"); final surname = signal("M"); final fullName = computed(() => name.value + "-" + surname.value); // Logs: "Jane Doe" effect(() => print(fullName.value)); // Updating one of its dependencies will automatically trigger // the effect above, and will print "John Doe" to the console. name.value = "D";

上述代码会先打印N-M,然后会打印D-M,因为在最后执行name.value = "D";时:

  • effect里的函数会被调用,因为它内部有fullName.value,signals 内部会自动跟踪fullName的状态变化

  • computed会被调用,因为computedfullName.valueeffect内被访问,所以name的数值发生改变,从而让computed需要刷新状态

是不是有点懵?这其实就是 signals 的 「魔法」,它的独特之处在于,它是「自动状态绑定」和「自动依赖跟踪」

❝ 和其他传统的状态管理模型不同在于,signals 支持开发者精确地跟踪状态变化并仅更新依赖于这些变化的部分 UI,就像上面的代码,「自动化」的实现看起来就像是「魔法」。

但是,事实上当你觉得某个框架是「魔法」时,那其实这个框架并不适合你使用,毕竟当遇到「咒语」失灵时,「魔法师」就很容易成为「脆皮的废物」,所以搞清楚 signals 的「魔法」实现原理尤为重要。

前言

开始解析在聊 signals.dart 之前,需要快速介绍 signals 的前置概念,附带还有 Preact、Preact Signals 、SolidJS 等关键词。

首先需要说明一点,「Signals」 是业内通用的一种状态管理模式,而 signals.dart 项目就是 Preact Signals 的一个 Dart 移植版本,所以在最底层源码里你可以看到 Preact Signals 的核心原语,自然也就是包含了Signal 的细粒度、惰性求值和自动依赖追踪等能力

那么 Preact、 Preact Signals 又是什么,还有一开始图片提到的「类似 solidjs 状态管理」,它们和 signals.dart 有什么关系?

首先我们说过,「Signals」 是一种概念模式,它并不限制于任何语言还有框架,而在这个基础上:

  • Preact 是一个轻量级的 React 替代方案

  • Preact Signals 是 Preact 团队基于 Signals 概念提供的可用于 Preact 和 React 状态管理

  • SolidJS 是一个围绕 Signals 模式实现的 UI 框架,它是完全基于 Signals 驱动的框架

所以在 signals.dart 的源码和资料里都能看到它们的身影,而事实上signals.dart 的实现就深受 Preact Signals 的影响,比如最底层的基础代码结构上:

而对于 Signals 而言,它的主要优势在于更高校的颗粒度更新、自动化实现依赖跟踪、延迟计算等特点,其中我们最需要理解的,就是自动化实现依赖跟踪的「魔法」。

解析

要搞清楚「魔法」,首先我们需要知道effect是如何工作,如下代码所示,可以看到先打印输出了N,然后在value被改变的时候,又输出了D,那为什么在name.value改变的时候,effect 就会被调用呢?

这就不得不提,在 signals 里.value的 setter 和 getter 方法都是有特殊处理的,简单来说,就是当 value 被调用时,就会触发相应的逻辑,比如:「创建出对应的Node」,其实对于 signals 来说,内部Node是一个很重要的概念,因为它的实现基础,都是基于这个内部Node双链表来完成

其实,在signals.dartNode一直扮演着核心角色,它是自动跟踪依赖和管理状态结构的基础模块 ,比如Node类通过将ReadonlySignal(数据源)连接到对应的ComputedEffect等数据「消费者」来完成依赖:

class Node { // 目标依赖的源。 final ReadonlySignal _source;   Node? _prevSource;   Node? _nextSource; // 依赖源并在源改变时应被通知的目标, 是消费者 final Listenable _target;   Node? _prevTarget;   Node? _nextTarget; // 目标上次看到的 _source 的版本号,使用版本号而不是存储源值, // 因为源值可能占用任意大小的内存,并且计算可能会因为惰性求值而永远持有它们, // 使用特殊值 -1 来标记可能未使用但可回收的节点。 int _version;
抽象概念

先聊它的抽象概念,本质上Node就是在SignalComputedEffect等对象里被创建,并集成到一个双向链表中,当开始建立依赖关系时,比如在Computed/Effect访问Signal的值时,新的Node对象久会被创建,并添加到依赖项 (_prevSource/_nextSource) 和消费者 (_prevTarget/_nextTarget) 列表里。

也就是当你在Computed/Effect调用.value的 setter 和 getter 时,依赖追踪就会自动完成,从而创建一个新的Node,而后续的更新和触发执行,都是通过这个Node链表的遍历来完成。

❝ 所以 Node不仅仅是一个简单的数据结构,它通过将 Signal(数据源)连接到 Computed/ Effect消费者从而连接形成了一个图谱,其中一个节点的变化可以传播到其他节点,最终确保状态的一致更新。

所以在 signals 里,会利用Node对象来通知存储在targets列表中的所有依赖者 ,当信号的值发生改变时,会遍历依赖者列表,并根据_version对比结果来触发更新。

因为比对详细数据太过费时费力,通过_version来代表数据版本,不一致版本则更新,这样更有效率:

❝ 当 Signal的值被设置时,它的版本号会递增,当依赖的 computed/ effect运行时,它会记录其读取的每个 Signal的版本,在重新评估之前可以检查记录的版本是否更改,如果没有则可以跳过重新评估,从而节省资源。

所以在这些链表遍历时,_version可以在值改变时更高效地通知依赖者。

是不是觉得有些抽象?没事,我们接下来通过源码来理解。

Effect

首先,Effect会使用Node对象来订阅其依赖的Signal,而首次Effect都会被立即运行,并在每次依赖项更改时被运行,那么这里有两个关键流程:

  • 首先Effect就自己执行一次

  • 然后Effect内的.value的调用就完成了数据的跟踪绑定

那么我们看Effect首先执行的时候经历了什么,通过源码可以知道,Effect每次执行内部都会执行一个start函数,它其中一个关键的作用就是evalContext = this

这里的evalContext其实就是Computed/Effect的抽象上下文,它代表的是当前的执行环境,它是存在于global.dart里的全局变量,决定当前执行的上下文环境,evalContext = this大概意思就是 :

❝ Signal 现在执行到当前这个 Effect了。

也就是当Effect被执行的时候,evalContext就代表了当前的这个Effect,这就是Effect首次执行时的关键作用。

接下来就是Effect里的.value调用,让你调用Signal里 value 的 getter 时,其实内部就会对应调用addDependency给这个Signal添加依赖:

此时这个Effect就会创建出对应的Node,这个Node的 target 消费者evalContext正是当前Effect,可以看到,这就是自动跟踪的开始:

因为Effect首先被执行时,全局的evalContext会指向当前Effect,然后在Effect调用.value时,就会创建出Effect的对应Node,并添加到链表里。

❝ 所以自动跟踪的「魔法」,就在于 get value 里执行的依赖操作,通过读取当前执行环境 evalContext来判断需要依赖的位置。

那么,当我们执行.value =xxx的时候,同理就会触发 value 的执行 setter ,可以看到,此时相关 target (Effect) 就会被notify并最终执行endBatch

notify的作用就是把通过batchedEffect,把所有需要触发的Effect形成一个可访问链表,这里的头部batchedEffect也是一个全局对象:

而最终通过endBatch执行批处理,执行就会触发对应的Effect的 callback,进而再次执行到我们需要让他消费的地方,也就是effect里的函数因为 value 改变被再次执行:

这里有个叫needsToRecompute的函数,其实他就是分析数据源里面的所有version是否改变,如果有改变了,才执行Effect的 callback :

那么到这里,应该就可以简单理解Effect如何实现自动跟踪依赖和刷新:

  • 执行时通过全局对象指定当前evalContext

  • value 的 getter 和 setter 方法通过evalContext实现自动依赖跟踪

  • version 版本号判断是否更新

Computed

那么对于Computed来说也类似,不同的是 Computed 也是一个「特殊信号」,在获取它的 value 的时候同样会添加依赖,只是这里会有多一步internalRefresh操作:

internalRefresh其实就是一个判断是否需要更新的过程,比如用到前面的needsToRecompute会分析所有依赖项的 source version ,从而判断是否需要更新,还有evalContext = this切换到当前执行环境:

所以可以看到,对于Computed来说,更新数据其实不是主动的,它是在 value 被 getter 的时候,才会执行刷新计算,也就是它其实是懒加载的。

比如,在下面counter的 value 被调用之前,每次counter变化时,其实并不会主动触发computed, 而是当data.value被调用到时,有数据改变才会触发computed的的执行:

final data = computed(() {   return counter.value + 12; });

所以到这里,Computed的「魔法」实现你也了解了吧?除了自动依赖跟踪,应该也理解了为什么 signals 可以做到「颗粒度控制」和「性能优化」了吧?那接下来我们继续聊 Flutter Signals 。

Flutter

实际上,通过前面我们可以看出, signals 的状态管理可以说和 Flutter 没有「直接」关系,那它在 Flutter 上又是如何工作的?

首先我们看下方代码,这是一个最简单的 Flutter 使用 signals 的例子,这里的核心就是SignalsMixin

class _CounterExampleState extends State

  with SignalsMixin {   late final Signal

 counter = createSignal(0); void _incrementCounter() {     counter.value++;   } @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: const Text('Flutter Counter'),       ),       body: Center(         child: Column(           mainAxisAlignment: MainAxisAlignment.center,           children: [             const Text(               'You have pushed the button this many times:',             ),             Text(               '$counter',               style: Theme.of(context).textTheme.headlineMedium,             ),           ],         ),       ),       floatingActionButton: FloatingActionButton(         onPressed: _incrementCounter,         tooltip: 'Increment',         child: const Icon(Icons.add),       ),     );   } }

通过SignalsMixin,我们可以看到:

  • 首先是createSignal(0)创建信号而不是signal(0);

  • 直接使用 '$counter' 直接渲染数据

  • 改变counter.value,进而让 UI 更新

是不是很简单?这里的关键点就是createSignal(0),在SignalsMixin里调用createSignal的时候,内部会执行一个_watch操作,最终会在_setup的时候,在一个 effect 里订阅对应的 signal 的 value :

也就是说,当着value被改变时,它的effect就会被执行,从而触发_rebuild,进而执行setState更新控件

也就是createSignal是通过effect来让 UI 更新,这就是 signals 在 Flutter 里的最基础用法,类似的还有createEffectcreateComputed等,如果你需要实现自动监听和释放的话,那么在 Flutter 里最好就是使用SignalsMixin的各种 createXXX 方法,因为这样就可以做甩手掌柜:

❝ 为什么这么说?如果我们直接用 effect(() {xxx});,其实我们是需要手动执行 dispose ,不然比如页面销毁时, effect还会继续存在并且被执行。

另外 Flutter 还可以用的就是 signals 里的Watch控件,使用Watch就可以直接使用原始signal而不需要 createXXX :

final counter = signal(0); Watch.builder(builder: (context) {   return Text('$counter'); });

其实Watch内部是利用了createComputed做依赖跟踪,你在widget.builder的使用的 signal 都会被自动依赖到Computed,因为Watch内部是return result.value,所以在每次变化时,Computed都会重新刷新:

  late final result = createComputed(() {     return widget.builder(context, widget.child);   }, debugLabel: widget.debugLabel);   @override   Widget build(BuildContext context) {     return result.value;   }

另外还有counter.watch(context)方法,这个方法它会判断你是否存在SignalsMixin

  • 如果是直接监听即可

  • 如果不是,就获取 Flutter 的BuildContext并将当前的Element注册为Signal的监听器

而实际watch其实就是让value再变化时通过subscribe触发rebuild,另外这里它会使用signal.peek()来避免 value 调用时的 subscribing 监听。

peek()之所以不会被跟踪依赖,其实就是在返回 value 之前,先临时清空了evalContext,也就是没有执行环境了:

同样道理的还有batch批处理,其实也就是将全局的batchedEffect临时处理为空,并且判断batchDepth等操作:

举个例子,这里通过 signals 自己的SignalProvider实现将一个信号通过InheritedWidget往下共享,当然你可以也创建一个全局的 Signal ,这里展示的是:

  • 因为listen: false,所以不会主动更新

  • 所以此时counter.value ++并不会触发 Flutter 本身InheritedWidget的更新,自然也就不会更新到 UI

  • 但是此时effect里是可以正常打印

class _CounterExampleState extends State

  with SignalsMixin { void _incrementCounter() {     final counter = SignalProvider.of (context, listen:  false)!;     counter.value ++;   } @override   Widget build(BuildContext context) {     final counter = SignalProvider.of (context, listen:  false)!;     effect(() {       /// Register to $id AsyncSignal       print('counter id: ${counter.value}');     });     return Scaffold(       appBar: AppBar(         title: const Text('Flutter Counter'),       ),       body: Center(         child: Column(           mainAxisAlignment: MainAxisAlignment.center,           children: [             const Text(               'You have pushed the button this many times:',             ),             Text(               '$counter',               style: Theme.of(context).textTheme.headlineMedium,             ),           ],         ),       ),       floatingActionButton: FloatingActionButton(         onPressed: _incrementCounter,         tooltip: 'Increment',         child: const Icon(Icons.add),       ),     );   } }

从这里你也可以看到 signals 和 Flutter 之间的一个关系,signals 是一种数据跟踪和管理模式,而如何更新 Flutter UI ,就看你的颗粒度和使用需要,最方便的肯定是直接采用前面介绍的 API 。

❝ 毕竟手动销毁还是挺“麻烦”的。

同时,针对 Flutter 上的支持,signals 也提供了SignalProvider用于需要实现往下共享 Signal 的场景,但是本身 Signal 就支持 context 无关定义,所以实际上不用SignalProvider也可以,毕竟 Signal 本身的颗粒度控制会比InheritedWidget更细腻。

另外, signals 也并不强求什么写什么顶层容器,甚至也不需要InheritedWidget的支持,它单纯就是依赖自己内部驱动的概念,不管是局部状态管理,还是全局状态管理,它都可以很灵活。

最后,signals 也提供了 DevTools 上的数据可视化结构,这其实也是现在状态管理框架的标配之一了:

到这里我们就可以做个简单的总结了,在 signals 里最基础就是SignalComputedEffect,它们的实现逻辑可以简单总结为:

  • Computed/Effect运行时会通过全局evalContext标注当前运行环境

  • Signal的 value 对 getter 和 setter 有特殊处理,一般 getter 会根据evalContext自动添加依赖,而 setter 会刷新数据version并更新所有依赖Effect

  • Computed是一种特殊信号,它的懒加载决定了它只有在 value 被调用时才会触发刷新计算

  • peekbatched其实都是对全局环境变量的临时清空操作

  • version作为判断数据版本的主要依据

所以, 当Computed/Effect函数运行时, 可以做到追踪在函数中访问 value 的任何信号变化,对于每个被访问的信号,都会创建一个新的Node对象(或者重用现有的对象),从而将信号链接到当前的Computed/EffectNode会被添加到Computed/Effect的依赖项列表和信号的依赖者列表中。

这种自动订阅机制就是 signals 的关键「魔法」,通过消除手动声明依赖项的需求,简化了状态管理,甚至在 Flutter 可以一定程度”脱离“ Context 实现状态更新的实现原理。

那么,你会选择 signals.dart 吗?

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
王灿晒泳装照!配文:做了妈妈也可以穿衣自由,瘦成排骨上围有料

王灿晒泳装照!配文:做了妈妈也可以穿衣自由,瘦成排骨上围有料

悦君兮君不知
2026-03-14 02:08:15
白人女性与黑人女性的体味差异,网友真实分享引发热议

白人女性与黑人女性的体味差异,网友真实分享引发热议

特约前排观众
2025-12-22 00:20:06
官方通报“30平店面开出10家档口变美食城”:涉事公司已停业整改

官方通报“30平店面开出10家档口变美食城”:涉事公司已停业整改

界面新闻
2026-03-14 20:55:16
A股:今晚2.5亿股民,要超级兴奋了!!你知道是为什么吗?

A股:今晚2.5亿股民,要超级兴奋了!!你知道是为什么吗?

另子维爱读史
2026-03-14 20:30:16
中国女篮轻取南苏丹!王思雨下半场轰17分

中国女篮轻取南苏丹!王思雨下半场轰17分

体坛周报
2026-03-14 21:31:17
《镖人》卖不动,就是观众不想再听吴京“胡说八道”

《镖人》卖不动,就是观众不想再听吴京“胡说八道”

壹家言
2026-03-13 19:34:18
“死”了7年的爱泼斯坦还活着?被人拍到驾驶豪车,在美国公路上悠然兜风

“死”了7年的爱泼斯坦还活着?被人拍到驾驶豪车,在美国公路上悠然兜风

不掉线电波
2026-03-14 16:51:20
WTT重庆赛 | 决赛日到来,温瑞博挑战张本智和,蒯曼王艺迪对决

WTT重庆赛 | 决赛日到来,温瑞博挑战张本智和,蒯曼王艺迪对决

乒谈
2026-03-14 23:31:17
“美军红线”哈尔克岛:美伊合资打造的伊朗“王冠明珠”,关键、强韧,也脆弱

“美军红线”哈尔克岛:美伊合资打造的伊朗“王冠明珠”,关键、强韧,也脆弱

红星新闻
2026-03-14 15:58:37
央视紧急曝光:全是假货!别再往家里拎了,很多人天天在用!

央视紧急曝光:全是假货!别再往家里拎了,很多人天天在用!

鲸探所长
2026-03-14 10:53:33
5架美军加油机在沙特遭袭受损

5架美军加油机在沙特遭袭受损

界面新闻
2026-03-14 07:23:25
女单四强出炉!国乒下半区失守+各项3连败,石洵瑶不敌张本美和

女单四强出炉!国乒下半区失守+各项3连败,石洵瑶不敌张本美和

烧体坛
2026-03-14 21:49:29
在芭提雅失踪的中国女子被发现陈尸叻丕府椰子园

在芭提雅失踪的中国女子被发现陈尸叻丕府椰子园

曼谷陈大叔
2026-03-13 15:50:49
伊朗对美以发动47波攻击

伊朗对美以发动47波攻击

界面新闻
2026-03-14 07:13:39
食品安全到底谁守护,21吨冻干草莓检出剧毒农药,到底谁最毒

食品安全到底谁守护,21吨冻干草莓检出剧毒农药,到底谁最毒

西楼知趣杂谈
2026-03-14 16:15:52
美军猛炸伊朗地下导弹长城,疑似十万伊军被埋地下?

美军猛炸伊朗地下导弹长城,疑似十万伊军被埋地下?

高博新视野
2026-03-14 07:45:10
如果把钢筋混凝土中的铁换成钛,会怎么样?建筑会变得更牢固吗?

如果把钢筋混凝土中的铁换成钛,会怎么样?建筑会变得更牢固吗?

怪罗
2026-03-12 14:09:23
停止一切拨款!中科院正式向全世界宣布终止,西方学界已哀嚎一片

停止一切拨款!中科院正式向全世界宣布终止,西方学界已哀嚎一片

离离言几许
2026-03-12 18:23:19
农民自愿永久退出承包地:2026最新补偿标准与办理流程全说明

农民自愿永久退出承包地:2026最新补偿标准与办理流程全说明

现代小青青慕慕
2026-03-14 12:56:29
丢人丢到国外!中国男子在肯尼亚机场被拦,行李中发现2238只蚁后

丢人丢到国外!中国男子在肯尼亚机场被拦,行李中发现2238只蚁后

万象硬核本尊
2026-03-14 20:02:47
2026-03-15 04:11:00
君伟说
君伟说
分享职场故事
388文章数 48关注度
往期回顾 全部

科技要闻

xAI创始伙伴只剩两人!马斯克“痛改前非”

头条要闻

伊朗船只迫近林肯号航母 美军连开数炮全打空

头条要闻

伊朗船只迫近林肯号航母 美军连开数炮全打空

体育要闻

NBA唯一巴西球员,增重20KG顶内线

娱乐要闻

九成美曝田栩宁孕期出轨 AI反转引热议

财经要闻

3·15影子暗访|神秘的“特供酒”

汽车要闻

吉利银河M7技术首秀 实力重构主流电混SUV

态度原创

教育
家居
数码
时尚
手机

教育要闻

去英国留学的核心意义,其实80%以上国内家庭是不知道的!

家居要闻

艺术之家 法式优雅

数码要闻

AWE洗衣机观察:卷烘干、卷AI,「无感」洗衣才是未来?

伊姐周六热推:电视剧《逐玉》;电视剧《江湖夜雨十年灯》......

手机要闻

折痕没了!OPPO Find N6登陆线下门店 网友:这才叫无印良品

无障碍浏览 进入关怀版