内容简介:美团 EasyReact 源码剖析:图论与响应式编程(上):四、边的变换EZRTransform有很多衍生类,每一个都对应一种变换。什么叫变换呢?也就是在数据传到EZRTransform的时候,EZRTransform对数据进行处理,然后再按照特定的逻辑继续发送。
美团 EasyReact 源码剖析:图论与响应式编程(上):
四、边的变换
EZRTransform有很多衍生类,每一个都对应一种变换。什么叫变换呢?也就是在数据传到EZRTransform的时候,EZRTransform对数据进行处理,然后再按照特定的逻辑继续发送。
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。
实现 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; ) */
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 = @"于是他们幸福的在一起了"; /* 打印如下: 旁白节点接到台词: 在一个宁静的下午 李雷节点接到台词: 李雷:大家好,我叫李雷。 韩梅梅节点接到台词: 韩梅梅:大家好,我叫韩梅梅。 李雷节点接到台词: 李雷:你好韩梅梅。 韩梅梅节点接到台词: 韩梅梅:你好李雷。 旁白节点接到台词: 于是他们幸福的在一起了 */
分支的实现几乎是最复杂的了,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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入剖析Vue源码 - 响应式系统构建(上)
- 美团 EasyReact 源码剖析:图论与响应式编程
- 从JavaScript属性描述器剖析Vue.js响应式视图
- 理解响应者和响应链如何处理事件
- 从源码解析vue的响应式原理-响应式的整体流程
- 【Java集合源码剖析】ArrayList源码剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。