基本变换

优质
小牛编辑
135浏览
2023-12-01

基本变换是一组一元变换形式,每次变换是由一个节点出发,经过计算向其下游节点进行传播的,最基本的fork操作就是如此,下面介绍下全部的基本变换形式。

map

map:方法是 EasyReact 相当常用的一个变换方法,它的作用是对上游节点的每一个非空值进行一次计算,并将得到的结果同步的传递给下游节点:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRNode<NSString *> *nodeB = [nodeA map:^NSString *(NSNumber *next){
  return next.stringValue;
}];

NSLog(@"%@", nodeB.value);                                            // <- @"1"
nodeA.value = @2;
NSLog(@"%@", nodeB.value);                                            // <- @"2"

有的时候,可能每次 map 的结果和当前传递的值并没有关系,这样我们就可以用mapReplace:来简单处理:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRNode<NSString *> *nodeB = [nodeA mapReplace:@"叮铃,有值啦!"];

[[nodeB listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"%@", next);
}];
nodeA.value = @2;
nodeA.value = @3;

/* 打印如下:
叮铃,有值啦!
叮铃,有值啦!
叮铃,有值啦!
 */

需要注意的是mapReplace:创建的边 EZRMapTransform 里面会强持有其入参,注意避免循环引用。

filter

filter:的作用是过滤每个上游的值,将符合条件的值传递给下游:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRNode<NSNumber *> *nodeB = [nodeA filter:^BOOL(NSNumber *next){
  return next.integerValue > 5;
}];

NSLog(@"%@", nodeB.value);                                            // <- EZREmpty()
nodeA.value = @6;
NSLog(@"%@", nodeB.value);                                            // <- @6
nodeA.value = @3;
NSLog(@"%@", nodeB.value);                                            // <- @6

对于过滤我们还有两个便捷的方法:ignore:select:,它们的作用是分别过滤相同的和不同的,例如:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRNode<NSNumber *> *nodeB = [nodeA ignore:@1];
EZRNode<NSNumber *> *nodeC = [nodeA select:@1];

[[nodeB listenedBy:self] withBlock:^(NSNumber *next) {
  NSLog(@"NodeB 收到 %@", next);
}];
[[nodeC listenedBy:self] withBlock:^(NSNumber *next) {
  NSLog(@"NodeC 收到 %@", next);
}];

nodeA.value = @12;
nodeA.value = @1;
nodeA.value = @7;

/* 打印如下:
NodeC 收到 1
NodeB 收到 12
NodeC 收到 1
NodeB 收到 7
 */

distinctUntilChanged

distinctUntilChanged方法将一个不传递重复值的变换传递给其衍生节点,例如:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode value:@1];
EZRNode<NSNumber *> *nodeB = [nodeA distinctUntilChanged];

[[nodeB listenedBy:self] withBlock:^(NSNumber *next) {
  NSLog(@"收到 %@", next);
}];

nodeA.value = @1;
nodeA.value = @2;
nodeA.value = @2;
nodeA.value = @1;
nodeA.value = @2;

/* 打印如下:
收到 1
收到 2
收到 1
收到 2
 */

throttle

节流描述了这样的一种操作,对于上游的值来说,在一定的时间内如果有新的值则不会传递旧的值,如果等待到指定的时间没有新的值再将之前的值传递到下游。由于传递是异步的,所以阀门操作一般需要指定一个 GCD 的队列来告诉 EasyReact 在哪里进行传递。

一般阀门的操作用于搜索输入这样的需求上用来避免多次请求网络:

EZRMutableNode<NSString *> *inputNode = [EZRMutableNode new];
EZRNode<NSString *> *searchNode = [inputNode throttle:1 queue:dispatch_get_main_queue()];             // <- 单位是秒

[[searchNode listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"想要搜索的是 %@", next);
}];

inputNode.value = @"h";
inputNode.value = @"he";
inputNode.value = @"hel";
inputNode.value = @"hell";
inputNode.value = @"hello";

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
  inputNode.value = @"hello ";
  inputNode.value = @"hello w";
  inputNode.value = @"hello wo";
  inputNode.value = @"hello wor";
  inputNode.value = @"hello worl";
  inputNode.value = @"hello world";
});

/* 打印如下:
想要搜索的是 hello
想要搜索的是 hello world
 */

大家通常都想要在主队列完成监听,所以throttleOnMainQueue:方法快速的提供了阀门到主队列的能力:

EZRMutableNode<NSString *> *inputNode = [EZRMutableNode new];
EZRNode<NSString *> *searchNode = [inputNode throttleOnMainQueue:1];

等价于

EZRMutableNode<NSString *> *inputNode = [EZRMutableNode new];
EZRNode<NSString *> *searchNode = [inputNode throttle:1 queue:dispatch_get_main_queue()];

skip

跳过操作顾名思义就是跳过前几个值:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
EZRNode<NSNumber *> *nodeB = [nodeA skip:2];
NSLog(@"%@", nodeB.value);                                            // <- EZREmpty()
nodeA.value = @1;
NSLog(@"%@", nodeB.value);                                            // <- EZREmpty()
nodeA.value = @2;
NSLog(@"%@", nodeB.value);                                            // <- EZREmpty()
nodeA.value = @3;
NSLog(@"%@", nodeB.value);                                            // <- @3

take

拿取操作顾名思义就是只拿取前几个值:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
EZRNode<NSNumber *> *nodeB = [nodeA take:2];
NSLog(@"%@", nodeB.value);                                            // <- EZREmpty()
nodeA.value = @1;
NSLog(@"%@", nodeB.value);                                            // <- @1
nodeA.value = @2;
NSLog(@"%@", nodeB.value);                                            // <- @2
nodeA.value = @3;
NSLog(@"%@", nodeB.value);                                            // <- @2

deliverOn

前面提到了在多线程下值的修改和监听是同一线程的,我们也可以使用withBlock:on或者withBlockOnMainQueue在监听的时候进行处理。但是如果在变换的过程中耗时非常长,或者遇到变换中必须在主线程的时候,只在监听上做处理已经满足不了需要了:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
EZRNode<NSNumber *> *nodeB = [nodeA map:^NSNumber *(NSNumber *next) {
    [NSThread sleepForTimeInterval:next.doubleValue];
    return next;
}];
EZRNode<NSNumber *> *nodeC = [nodeB map:^NSNumber *(NSNumber *next) {
    NSAssert([[NSThread currentThread] isMainThread], @"");
    return next;
}];
[[nodeC listenedBy:self] withBlock:^(NSNumber * _Nullable next) {
}];
nodeA.value = @(9.0);
// 哇,又要等一会了
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    nodeA.value = @3; // 不好,要断言失败了
});

这时deliverOn:deliverOnMainQueue就派上用场了:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
dispatch_queue_t queue = dispatch_queue_create("someQueue", DISPATCH_QUEUE_SERIAL);
EZRNode<NSNumber *> *nodeB = [[nodeA deliverOn:queue] map:^NSNumber *(NSNumber *next) {
    [NSThread sleepForTimeInterval:next.doubleValue];
    return next;
}];
EZRNode<NSNumber *> *nodeC = [[nodeB deliverOnMainQueue] map:^NSNumber *(NSNumber *next) {
    NSAssert([[NSThread currentThread] isMainThread], @"");
    return next;
}];
[[nodeC listenedBy:self] withBlock:^(NSNumber * _Nullable next) {
}];
nodeA.value = @(9.0);
// 嗯,不担心
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    nodeA.value = @3; // 嗯,不担心
});

delay

延迟操作顾名思义就是推迟一段时间后传递给下游节点,由于传递的时候已经找不到之前上游设置的线程,所以延迟操作需要一个 GCD 的队列来派发传递的任务:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
EZRNode<NSNumber *> *nodeB = [nodeA delay:1 queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
EZRNode<NSNumber *> *nodeC = [nodeA delayOnMainQueue:2];

scan

扫描操作是个稍微复杂一点的操作,它需要传入一个初始值和一个带有两个入参的 block。当上游第一次有值传递过来的时候,会以初始值和上游当前值调用这个 block,block 的返回值就是下游的值并且这个值会被记下来。以后每次上游有值传递过来的时候,都会以上一次记下来的值和上游当前值调用这个 block,以此循环。例如:

EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
EZRNode<NSMutableArray<NSNumber *> *> *nodeB = [nodeA scanWithStart:[NSMutableArray array] reduce:^NSMutableArray *(NSMutableArray *last, NSNumber *current) {
  [last addObject:current];
  return last;
}];
[[nodeB listenedBy:self] withBlock:^(NSMutableArray *array) {
  NSLog(@"接收到 %@", array);
}];
nodeA.value = @1;
nodeA.value = @2;
nodeA.value = @3;
nodeA.value = @4;
nodeA.value = @5;
/* 打印如下:
接收到 (
    1
)
接收到 (
    1,
    2
)
接收到 (
    1,
    2,
    3
)
接收到 (
    1,
    2,
    3,
    4
)
接收到 (
    1,
    2,
    3,
    4,
    5
)
 */

其过程如下:

upstream:  -----------1-----------2-----------3-----------4-----------5
                      |           |           |           |           |
start:            []  |           |           |           |           |
                    ↘ ↓           ↓           ↓           ↓           ↓
downstream: ---------[1]-------→[1,2]-----→[1,2,3]---→[1,2,3,4]-→[1,2,3,4,5]