美团 EasyReact 源码剖析:图论与响应式编程(下)

栏目: IOS · 发布时间: 5年前

内容简介:美团 EasyReact 源码剖析:图论与响应式编程(上):四、边的变换EZRTransform有很多衍生类,每一个都对应一种变换。什么叫变换呢?也就是在数据传到EZRTransform的时候,EZRTransform对数据进行处理,然后再按照特定的逻辑继续发送。

美团 EasyReact 源码剖析:图论与响应式编程(上):

四、边的变换

EZRTransform有很多衍生类,每一个都对应一种变换。什么叫变换呢?也就是在数据传到EZRTransform的时候,EZRTransform对数据进行处理,然后再按照特定的逻辑继续发送。

美团 EasyReact 源码剖析:图论与响应式编程(下)

EasyReact 自带有非常多的变换处理,比如map、filter、scan、merge等,可以到 GitHub 查看其使用,也可以直接查看源码,大多数的变换的实现都是很简单易懂的,笔者这里只列举并解析几个稍微比较复杂的实现(主要是通过结构图来解析,最好是对照源码理解)。

combine

响应式编程经常会使用 a := b + c 来举例,意图是当 b 或者 c 的值发生变化的时候,a 会保持两者的加和。那么在响应式库 EasyReact 中,我们是怎样体现的呢?就是通过 EZRCombine-mapEach 操作:

EZRMutableNode *nodeA = [EZRMutableNode value:@1];
EZRMutableNode *nodeB = [EZRMutableNode value:@2];
EZRNode *nodeC = [EZRCombine(nodeA, nodeB) mapEach:^NSNumber *(NSNumber *a, NSNumber *b) {
  return @(a.integerValue + b.integerValue);
}];
nodeC.value;           // <- 1 + 2 = 3
nodeA.value = @4;
nodeC.value;           // <- 4 + 2 = 6
nodeB.value = @6;
nodeC.value;           // <- 4 + 6 = 10

上面是官方的描述和例子,实际上 combine 操作就是nodeC的值始终等于nodeA + nodeB。

美团 EasyReact 源码剖析:图论与响应式编程(下)

实现 combine 的边叫做EZRCombineTransform,同时有一个EZRCombineTransformGroup作为处理器,它持有了所有相关的边,当数据经过EZRCombineTransform时,交由处理器将所有边的值相加,然后继续发送。

zip

拉链操作是这样的一种操作:它将多个节点作为上游,所有的节点的第一个值放在一个元组里,所有的节点的第二个值放在一个元组里……以此类推,以这些元组作为值的就是下游。它就好像拉链一样一个扣着一个:

EZRMutableNode *nodeA = [EZRMutableNode value:@1];
EZRMutableNode *nodeB = [EZRMutableNode value:@2];
EZRNode *nodeC = [nodeA zip:nodeB];

[[nodeC listenedBy:self] withBlock:^(EZTuple2 *tuple) {
  NSLog(@"接收到 %@", tuple);
}];
nodeA.value = @3;
nodeA.value = @4;
nodeB.value = @5;
nodeA.value = @6;
nodeB.value = @7;
/* 打印如下:
接收到 (
  first = 1;
  second = 2;
  last = 2;
)
接收到 (
  first = 3;
  second = 5;
  last = 5;
)
接收到 (
  first = 4;
  second = 7;
  last = 7;
)
 */

美团 EasyReact 源码剖析:图论与响应式编程(下)

zip 的数据结构实现和 combine 如出一辙,不同的是,每一个EZRZipTransform都维护了一个新值的队列,当数据流动时,EZRZipTransformGroup会读取每一个边对应队列的顶部元素(同时出队),若某一个边的队列未读取到新值则停止数据传播。

switch

switch-case-default 变换是通过给出的 block 将每个上游的值代入,求出唯一标识符,再分离这些标识符的一种操作。我们举例一个分离剧本的例子:

EZRMutableNode *node = [EZRMutableNode new];
EZRNode *nodes = [node switch:^id _Nonnull(NSString * _Nullable next) {
  NSArray *components = [next componentsSeparatedByString:@":"];
  return components.count > 1 ? components.firstObject: nil;
}];
EZRNode *liLeiSaid = [nodes case:@"李雷"];
EZRNode *hanMeimeiSaid = [nodes case:@"韩梅梅"];
EZRNode *aside = [nodes default];
[[liLeiSaid listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"李雷节点接到台词: %@", next);
}];
[[hanMeimeiSaid listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"韩梅梅节点接到台词: %@", next);
}];
[[aside listenedBy:self] withBlock:^(NSString *next) {
  NSLog(@"旁白节点接到台词: %@", next);
}];
node.value = @"在一个宁静的下午";
node.value = @"李雷:大家好,我叫李雷。";
node.value = @"韩梅梅:大家好,我叫韩梅梅。";
node.value = @"李雷:你好韩梅梅。";
node.value = @"韩梅梅:你好李雷。";
node.value = @"于是他们幸福的在一起了";
/* 打印如下:
旁白节点接到台词: 在一个宁静的下午
李雷节点接到台词: 李雷:大家好,我叫李雷。
韩梅梅节点接到台词: 韩梅梅:大家好,我叫韩梅梅。
李雷节点接到台词: 李雷:你好韩梅梅。
韩梅梅节点接到台词: 韩梅梅:你好李雷。
旁白节点接到台词: 于是他们幸福的在一起了
 */

美团 EasyReact 源码剖析:图论与响应式编程(下)

分支的实现几乎是最复杂的了,node首先通过EZRSwitchMapTransform边连接一个nodes下游节点,并且初始化一个分支划分规则 (block);然后nodes节点分别通过EZRCaseTransform边连接liLeiSaid、hanMeimeiSaid、aside下游节点,并且每一个下游节点存储了一个匹配分支的key(也就是例子中的“李雷”、“韩梅梅”等)。

当node发送数据过来时,由EZRSwitchMapTransform通过分支划分规则处理数据,然后将每一个分支节点通过 hash 容器装起来,也就是图中的蓝色节点case node,这个例子发送的数个消息最终会创建三个分支;在创建分支完成过后,EZRSwitchMapTransform向下游继续发送数据,在数据到达EZRCaseTransform时,该边会监听对应的case node(当然前提是匹配)而不会继续向下游发送数据;然后EZRSwitchMapTransform会继续改变对应case node的值,由此EZRCaseTransform就接收到了数据改变的通知,最终发送给下游节点,即这里的liLeiSaid、hanMeimeiSaid或aside。

笔者思考了一番,并没有找到必须使用case node节点的充分理由,可能是疏漏了某些细节,希望理解深刻的读者在文末留言。

五、代码细节及优化

在源码的阅读中,发现了几个有意思的代码技巧。

自动解锁

- (void)_next:(nullable id)value from:(EZRSenderList *)senderList context:(nullable id)context {
    {
        EZR_SCOPELOCK(_valueLock);
        _value = value;
    }
    ...
}

EZR_SCOPELOCK()宏的出场率相当高,直接查看实现:

#define EZR_SCOPELOCK(LOCK) /
EZR_LOCK(LOCK);  /
EZR_LOCK_TYPE EZR_CONCAT(auto_lock_, __LINE__) /
__attribute__((cleanup(EZR_unlock), unused)) = LOCK

可以看到先是对传进来的锁进行加锁操作,后面关键的有句代码:

__attribute__((cleanup(AnyFUNC), unused))

这句代码加在局部变量后面,将会在局部变量作用域结束之前调用AnyFUNC方法。那么此处的目的很简单,看一眼这里的EZR_unlock干了什么:

static inline void EZR_unlock(EZR_LOCK_TYPE *lock) {
    EZR_UNLOCK(*lock);
}

具体的宏可以看源码,此处只是做了一个解锁操作,由此就实现了自动解锁功能。这就是为什么要用大括号把加锁的代码包起来,可以理解为限定加锁的临界区。

虽然少写句代码的意义不大,但是却比较炫。

分支预测

经常会看到类似的代码:

if EZR_LikelyNO(value == EZREmpty.empty) {
    ...
}

EZR_LikelyNO系列宏出场率也是极高的:

#define EZR_Likely(x)       (__builtin_expect(!!(x), 1))
#define EZR_Unlikely(x)     (__builtin_expect(!!(x), 0))
#define EZR_LikelyYES(x)    (__builtin_expect(x, YES))
#define EZR_LikelyNO(x)     (__builtin_expect(x, NO))

可以看到实际上就是__builtin_expect()函数的宏,!!(x)是为了把非 0 变量变为 1 。

我们知道 CPU 有流水线执行能力,当处理分支程序时,判断成功过后可能会产生指令的跳转,打断 CPU 对指令的处理,并且直到判断完成这个过程中,CPU 可能流水执行了大量的无用逻辑,浪费了时钟周期。

简单分析一下:

1 读取指令 | 执行指令 | 输出结果   (判断指令)
2           读取指令 | 执行指令 | 输出结果
3                     读取指令 | 执行指令 | 输出结果

假设一条指令的执行分为三个阶段,若这里是一个分支语句判断,第 1 行是判断指令,在判断指令输出结果时,下面两条指令已经在执行中了,而判断结构是走另外一个分支,这就必然需要跳转指令,而放弃 2、3 条指令的执行或结果。

那么怎样保证尽量不跳转指令呢?

答案就是分支预测,通过工程师对业务的理解,告知编译器哪个分支概率更大,比如:

if (__builtin_expect(someValue, NO)) {
    //为真代码
} else {
    //为假代码
}

那么在编译后,可执行文件中“为假代码”转换的指令将会靠前,优先执行。

后语

EasyReact 将图论与响应式编程结合起来表现非常好,将各种复杂逻辑都用相同的思维处理,不管从理解上还是使用上都非常具有亲和性。

不过 EasyReact 作为美团组件库中的一个组件来说是很合适的,但是如果作为一个独立的框架来说却显得有点臃肿了。

作为一个普通的开发者,可能更多的想如何高效且快捷的做一个框架,毕竟少有团队拥有美团的技术实力。比如框架依赖了 EasySequence,这个东西对于 EasyReact 来说没有太大意义,弱引用容器也可以用NSPointerArray替代;EasyTuple 元祖的实现有些复杂了,如果是个人框架的话建议使用 C++ 的 tuple;队列、链表等数据结构也不需自己实现,队列可以用 C++ 的queue,链表用 Objective-C 数组或 C 数组来表示也更加轻量。

这种从公司剥离的框架总是会有很多限制,比如公司的代码规范、类库使用规范,肯定远不及个人框架的自由和随性。

在 EasyReact 中也体会到了一些设计思维,从代码质量来说确实是上乘的,阅读过程中非常的流畅,很多看起来简单的实现,细想过后能发现令人惊喜的作用。

整体来说,收获颇丰,给美团技术团队点个赞。

作者:indulge_in

链接:https://www.jianshu.com/p/78200101ef13


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Caching

Web Caching

Duane Wessels / O'Reilly Media, Inc. / 2001-6 / 39.95美元

On the World Wide Web, speed and efficiency are vital. Users have little patience for slow web pages, while network administrators want to make the most of their available bandwidth. A properly design......一起来看看 《Web Caching》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具