[Flutter小试牛刀] 写一个低配版的signals

signals是继privider,riverpod状态管理器之后的又一个热门; 它运用方便,理解简单,特别是已经对前端signals框架比较熟悉的同学。

文本旨在研究signals的原理,复刻一个"够用就行"的低配版状态管理框架。

signals 的核心用法就是 signal 和 computed 方法。

dart 复制代码
final a = signal('a');
final b = signal('b');
final c = computed(() => a.value + b.value);
final dispose = effect((){
    print("c is ${c.value}");
});
expect(c.value, 'ab');
a.value = 'aa';
expect(c.value, 'aab');
dispose();

从上面的代码可以看出,我们在改变a的值后,c的值也随之改变。当c值改变时会触发effect的回调。 在flutter中我们可以将effect方法看作是 build方法,返回Wdiget树,那么当我们改变某个signal值时就会触发build重绘widget。

有意思的是,effect方法实际上监听的只是c的值,之所以改变a会触发c的改变是因为competed方法里c的值用到了a.value。

因此singals的核心思想就是这种链式监听。

初学者会感觉这像是一种魔法,因为c.value并没有传入任何的变量,那c和a,b等是如何实现关联监听的呢? 从singals源码中可以一窥究竟:

dart 复制代码
@override
T get value {
    final node = addDependency(this);

    if (node != null) {
        node.version = this.version;
    }
    return this.internalValue;
}

Node? addDependency(ReadonlySignal signal) {

    if (evalContext == null) {
        return null;
    }
    var node = signal.node;
    if (node == null || node.target != evalContext) {
        node = Node()
        ..version = 0
        ..source = signal
        ..prevSource = evalContext!.sources
        ..nextSource = null
        ..target = evalContext!
        ..prevTarget = null
        ..nextTarget = null
        ..rollbackNode = node;
        if (evalContext!.sources != null) {
            evalContext!.sources!.nextSource = node;
        }
        evalContext!.sources = node;
        signal.node = node;

        if ((evalContext!.flags & TRACKING) != 0) {
            signal.subscribeToNode(node);
        }
        return node;

    } else if (node.version == -1) {
        node.version = 0;
        if (node.nextSource != null) {
            node.nextSource!.prevSource = node.prevSource;
        if (node.prevSource != null) {
            node.prevSource!.nextSource = node.nextSource;
        }

        node.prevSource = evalContext!.sources;
        node.nextSource = null;
        evalContext!.sources!.nextSource = node;
        evalContext!.sources = node;
    }
        return node;
    }
    return null;
}

以上是Signal类中截取的获取value的方法,可以看出它调用了addDependency返回了一个node。而在addDependency则对每个Signal初始化了一个Node对象用来管理Signal之间的关系,从代码实现来看Node对象之间是通过双向链表实现关联的。且从代码中还可以发evalContext这个引用频繁出现,且是一个全局变量。

我们再看看Computed的实现:

dart 复制代码
@override
T get value {
    if ((flags & RUNNING) != 0) {
        throw Exception('Cycle detected');
    }
    final node = addDependency(this);
    internalRefresh();
    if (node != null) {
        node.version = version;
    }
    if ((flags & HAS_ERROR) != 0) {
        throw error!;
    }
    return _internalValue;
}

bool internalRefresh() {
    this.flags &= ~NOTIFIED;
    if ((this.flags & RUNNING) != 0) {
        return false;
    }

    if ((this.flags & (OUTDATED | TRACKING)) == TRACKING) {
        return true;
    }

    this.flags &= ~OUTDATED;
    if (this.internalGlobalVersion == globalVersion) {
        return true;
    }

    this.internalGlobalVersion = globalVersion;
    this.flags |= RUNNING;
    if (version > 0 && !needsToRecompute(this)) {
        this.flags &= ~RUNNING;
        return true;
    }
    final prevContext = evalContext;
    try {
        prepareSources(this);
        evalContext = this;
        final val = this.fn();
        if (!_isInitialized ||
            (flags & HAS_ERROR) != 0 ||
            _internalValue != val ||
            version == 0) {
            internalValue = val;
            flags &= ~HAS_ERROR;
            version++;
        }
    } catch (err) {
        error = err;
        flags |= HAS_ERROR;
        version++;
    }
    evalContext = prevContext;
    cleanupSources(this);
    flags &= ~RUNNING;
    return true;
}

代码很长,我们可以看到Computed和Signal的value对象一样调用了addDependency,之后调用了internalRefresh方法,在internalRefresh方法里我们又看到evalContext的身影,我们发现在调用 final val = this.fn();前它:evalContext = this;调用结束后又重制回来了:evalContext = prevContext;

从上面代码我们不难看出,Signals的魔法就在这里通过全局变量evalContext临时存储上下文信息,执行结束后又重置回来。 魔法就此揭开了。也许有人会问,这样设置全局变量不会有并发问题吗?答案是不会,因为dart的运行时isolate是单线程模型,它运行的实际上是按照顺序执行代码块,这里不详细展开讲解,可以自行查isolate的执行原理。


OK,那么我们如何在flutter中实现一个简单的像signals里signal方法和computed方法呢?首先我们也需要写一个Node用来管理调用链之间的关系,这里为了简单只设计两层的调用链,也就是computed只允许signal,不允许computed嵌套使用。他们的关系就先用树结构进行管理:

dart 复制代码
mixin class Node {
    //关联的子节点用Set是为了避免节点重复加入
    final Set<Node> children = {};
    //添加子节点
    void addChild(Node node) {
        children.add(node);
    }
    //删除子节点
    void removeChild(Node node) {
        children.remove(node);
    }
    //通知子节点改变
    void notify() {
        for (var child in children) {
            child.notify();
        }
    }
    //销毁
    void dispose() {
        children.clear();
    }
}

有了节点,我们就先写Singal,这里为了和Signal区分就用UseValue代替,为了方便就直接用Flutter的ValueNotifier:

dart 复制代码
typedef DisposeFn = void Function();

typedef ComputeFn<T> = T Function();

Node? childNode;

class UseValue<T> extends ValueNotifier<T> with Node {
    final DisposeFn? disposeFn;
    UseValue(super.value, {this.disposeFn});
    
    @override
    T get value {
        if (childNode != null) {
            addChild(childNode!);
        }
        var value = super.value;
        return value;
    }

    @override
    void dispose() {
        disposeFn?.call();
        super.dispose();
    }

    @override
    void notifyListeners() {
        super.notifyListeners();
        notify();
    }
}

在以上代码中我们定义了一个childNode全局对象,这个跟signals里的evelContext是一个作用,用于关联节点之间的关系。在T get value方法里,会去判断childNode是否为空,如果不为空则会将它加入到自己的children里,如果自己的之值发生改变则会去通知children也去改变。

有了Signal再写Computed,为了区分,我这里用UseComputed代替:

dart 复制代码
class UseComputed<T> extends ChangeNotifier with Node {
    final ComputeFn<T> computeFn;
    UseComputed(this.computeFn);
    T get value {
        var oldChildNode = childNode;
        try {
            childNode = this;
            var computeValue = computeFn();
            return computeValue;
        } finally {
            childNode = oldChildNode;
        }
    }

    @override
    void notify() {
        notifyListeners();
    }
}

从上面代码中 T get value 的实现可以看出,我们会把全局变量 childNode暂时缓存并替换成自己,执行完回调函数后又重置回去。

我们再写一个State的实现:

dart 复制代码
mixin MixinUseState<W extends StatefulWidget> on State<W> {
    final Set<DisposeFn> _disposeFnSet = {};
    @override
    void dispose() {
        for (var disposeFn in _disposeFnSet) {
            disposeFn.call();
        }
        super.dispose();
    }
    
    void _notifyChanged() {
        setState(() {});
    }
    
    //类似 signals里的 signal 方法
    UseValue<T> use<T>(T value,{bool addListener}) {
        var useValue = UseValue<T>(value);
        if(addListener){
            useValue.addListener(_notifyChanged);
        }
        _disposeFnSet.add(useValue.dispose);
        return useValue;
    }
    //类似 signals里的 computed 方法
    UseComputed<T> computed<T>(ComputeFn<T> computeFn) {
        var useComputed = UseComputed(computeFn);
        _disposeFnSet.add(useComputed.dispose);
        useComputed.addListener(_notifyChanged);
        return useComputed;
    }

}

在上面代码中,还添加了自动dispose的功能,调用use和compted时会将dispose方法放入到 _disposeFnSet中。

再写个Demo:

dart 复制代码
    void main() {
        runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
        const MyApp({super.key});
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                title: 'Flutter Demo',
                theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
                useMaterial3: true,
                ),
                home: const MyHomePage(title: 'Flutter Demo Home Page'),
            );
        }
    }

    class MyHomePage extends StatefulWidget {
        const MyHomePage({super.key, required this.title});
        final String title;
        @override
        State<MyHomePage> createState() => _MyHomePageState();
    }


    class _MyHomePageState extends State<MyHomePage> with MixinUseState {
        late final c1 = use(0);
        late final c2 = use(2);
        late final counter = computed(() {
            return c1.value + c2.value;
        });

        @override
        Widget build(BuildContext context) {
            return Scaffold(
                appBar: AppBar(
                    backgroundColor: Theme.of(context).colorScheme.inversePrimary,
                    title: Text(widget.title),
                ),
                body: Center(
                    child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                            const Text(
                            'You have pushed the button this many times:',
                            ),
                            Text(
                                '${counter.value}',
                                style: Theme.of(context).textTheme.headlineMedium,
                            ),
                        ],
                    ),),
               floatingActionButton: FloatingActionButton(
                    onPressed: () {
                        c1.value++;
                    },
                    tooltip: 'Increment',
                    child: const Icon(Icons.add),
                    ), // This trailing comma makes auto-formatting nicer for build methods.
                );
            }
    }

在测试代码中,定义了 c1,c2,c3,首先c1和c2类似于signal,而c3则是computed,我们在改变c1.value时会触发c3的改变,进行重新计算,并刷新UI。

自此这个简单的demo就实现完成了,但signals的功能远不止这些,例如batch可以同时改变多个值减少刷新次数,containner,类似riverpod里的family等。

相关推荐
GeniuswongAir7 小时前
Flutter BloC 架构入门指南
flutter·bloc
90后的晨仔10 小时前
Flutter 报错 [☠] Network resources (the doctor check crashed)xxxx
前端·flutter
pengyu11 小时前
【Flutter 状态管理 - 贰】 | 提升对界面与状态的认知
android·flutter·dart
HuWentao16 小时前
你不需要那么多Provider——重新理解状态管理与业务逻辑
前端·flutter
好的佩奇16 小时前
Dart 之异步模型
android·flutter·dart
louisgeek1 天前
Flutter StatelessWidget 和 StatefulWidget 的区别
flutter
JarvanMo1 天前
Flutter插件中引用aar
flutter
科昂2 天前
Dart 单线程异步模型:从原理到工程实践的系统化解析
android·flutter·dart