[Flutter小试牛刀] 低配版signals,添加batch

83 阅读2分钟

本文整个系列文章有:

在上篇文章里,已经写了一个减配版的signals,且添加了局部刷新的功能,这已经能够满足大部分的需求。但是某些时候,多个值的改变都会刷新某一部分UI,如果连续改动它们的值就会造成重复刷新的问题;

我们可以看到下面signals的一段代码:

final a = signal(0); 
final b = signal(1); 
final c = computed(() => a.value + b.value); 

effect((){
    print("c is ${c.value}");
});
a.value ++;
b.value ++;

当a值与b值的改变时都会影响c值的改变。我们同时改变了a 和 b的值,effect函数回调了两次。但某些情况下为了性能,我们只需要它回调一次就够了。

为了解决这个问题,signals添加了batch函数:

final a = signal(0); 
final b = signal(1); 
final c = computed(() => a.value + b.value); 

effect((){
    print("c is ${c.value}");
});

batch((){
    a.value++;
    b.value++;
});

上面带代码基于之前的代码添加了些许改进,在batch函数里对值进行修改,effect函数只会触发一次改动。

那么如何在我们简化版的signals里实现batch功能呢?

具体的实现思路如下:

  1. 记录batch开始
  2. 触发batch开始函数
  3. 触发batch函数过程中如果有刷新回调则将回调函数添加到batch回调列表。
  4. 记录batch结束
  5. 触发batch回调函数列表

有了上述的思路开始改造代码:

UseBatch? _batchNode;

class UseBatch extends Node {
    final VoidCallback fn;
    final Set<void Function()> _listeners = {};
    UseBatch(this.fn);
    @override
    void dispose() {
        _listeners.clear();
        _unsubscribe();
    }

    @override
    void notifyListeners() {
        for (var listener in _listeners) {
            listener.call();
        }
    }

    @override
    void addListener(void Function() listener) {
        _listeners.add(listener);
    }

    @override
    void removeListener(void Function() listener) {
        _listeners.remove(listener);
    }

    void call() {
        //判断是否已经处在batch,如果是则停止执行
        if (_batchNode != null) {
            return;
        }
        //将batch节点设置成自己
        _batchNode = this;
        try {
            //触发batch函数
            fn.call();
        } catch (e) {
            rethrow;
        } finally {
            //重置batch函数
            _batchNode = null;
            //触发batch回调函数列表
            notifyListeners();
            //清理
            dispose();
        }
    }
}

abstract class MixinUseState<W extends StatefulWidget> extends State<W> {
    ...
    
    //batch函数
    void batch(VoidCallback fn) {
        UseBatch(fn)();
    }
    
    void _notifyListeners() {
        //判断当前是否在batch函数下,如果是则将UI改变暂存,因为listeners是set,相同的回调函数只会回调一次
        if (_batchNode != null) {
            _batchNode?.addListener(_notifyListeners);
            return;
        }
        setState(() {});
    }
}

该改动的核心是我们知道所有触发UI改动的函数是 _notifyListeners ,我们可以在触发_notifyListeners前判断是否有 _batchNode ,如果有则延后触发。