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

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.

相关推荐
热点推荐
两会结束后,不出意外的话,未来两年房地产市场或将迎来3个变化

两会结束后,不出意外的话,未来两年房地产市场或将迎来3个变化

米果说识
2026-03-12 17:15:42
男人最懂男人:张嘉倪 vs 邵晴,买超选邵晴,早有猫腻

男人最懂男人:张嘉倪 vs 邵晴,买超选邵晴,早有猫腻

草莓解说体育
2026-03-12 14:53:34
他曾任四川省委副书记、省政协主席,一生扎根彝乡,享年89岁

他曾任四川省委副书记、省政协主席,一生扎根彝乡,享年89岁

王嚾晓
2026-03-15 03:15:10
折痕没了!OPPO Find N6登陆线下门店 网友:这才叫无印良品

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

快科技
2026-03-14 22:52:08
绝境突围!霍尔木兹海峡中国船员:要么饿死,要么冲过去

绝境突围!霍尔木兹海峡中国船员:要么饿死,要么冲过去

戗词夺理
2026-03-14 14:20:51
卢卡申科:谁敢干涉内政 就用“榛树”导弹伺候

卢卡申科:谁敢干涉内政 就用“榛树”导弹伺候

看看新闻Knews
2026-03-15 00:49:01
伊朗全球首次超重型高超音速子母弹实战,其性能世界排名如何?

伊朗全球首次超重型高超音速子母弹实战,其性能世界排名如何?

止戈军是我
2026-03-14 19:46:09
54岁艾美奖女演员下海:仅用75分钟还清房贷

54岁艾美奖女演员下海:仅用75分钟还清房贷

小椰的奶奶
2026-03-08 21:03:06
广东男篮官宣崔永熙复出,莫兰德公开炮轰杨鸣,郭艾伦将进行手术

广东男篮官宣崔永熙复出,莫兰德公开炮轰杨鸣,郭艾伦将进行手术

中国篮坛快讯
2026-03-14 14:46:55
开国中将看上一女兵,托人去说媒,女兵:我参军不是给人当老婆的

开国中将看上一女兵,托人去说媒,女兵:我参军不是给人当老婆的

Ck的蜜糖
2026-03-14 18:48:32
灭国级轰炸正拉开序幕,生死存亡时刻,伊朗总统却已指挥不动军队

灭国级轰炸正拉开序幕,生死存亡时刻,伊朗总统却已指挥不动军队

快看张同学
2026-03-10 10:33:24
古人为啥要定都西安?

古人为啥要定都西安?

喜之春
2026-03-12 06:21:08
小杨阿姨彻底不演了!自曝未复工并非家有喜事,马筱梅谎言被戳破

小杨阿姨彻底不演了!自曝未复工并非家有喜事,马筱梅谎言被戳破

潮鹿逐梦
2026-03-12 16:57:05
特朗普专机等着飞,鲁比奥被卡门外,西方称“耻辱”,北京不吭声

特朗普专机等着飞,鲁比奥被卡门外,西方称“耻辱”,北京不吭声

不要把蜜语说给侧耳听
2026-03-15 03:22:49
图书馆已成为性骚扰重灾区,不小心就会看见鸟

图书馆已成为性骚扰重灾区,不小心就会看见鸟

beebee
2026-03-13 11:04:01
巴黎现场太真实!Lisa三角区尴尬,全智贤状态差,刘亦菲也翻车了

巴黎现场太真实!Lisa三角区尴尬,全智贤状态差,刘亦菲也翻车了

一娱三分地
2026-03-12 19:11:45
迪拜用40年建设,11天就崩了

迪拜用40年建设,11天就崩了

贩财局
2026-03-14 09:05:51
0.4秒天堑难逾越!汉密尔顿确认法拉利正赛没戏,曝梅奔藏有绝招

0.4秒天堑难逾越!汉密尔顿确认法拉利正赛没戏,曝梅奔藏有绝招

体育妞世界
2026-03-14 23:33:01
一只苍蝇困死整个大陆?无法种地不能养马,这才是非洲的穷根!

一只苍蝇困死整个大陆?无法种地不能养马,这才是非洲的穷根!

你是我心中最美星空
2026-02-27 07:53:59
遭遇伤病潮!勇士10天合同签2米11中锋 G联赛场均23+13

遭遇伤病潮!勇士10天合同签2米11中锋 G联赛场均23+13

罗说NBA
2026-03-15 05:24:14
2026-03-15 05:44:50
君伟说
君伟说
分享职场故事
388文章数 48关注度
往期回顾 全部

科技要闻

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

头条要闻

伊朗“命根子”遭到中东史上最大轰炸 特朗普表态

头条要闻

伊朗“命根子”遭到中东史上最大轰炸 特朗普表态

体育要闻

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

娱乐要闻

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

财经要闻

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

汽车要闻

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

态度原创

时尚
健康
艺术
游戏
公开课

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

转头就晕的耳石症,能开车上班吗?

艺术要闻

这是唯一存世的毛主席画作

FS社新作终于有新消息!NS2独占 多人在线

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版