EasyReact 设计思维
EasyReact 设计思维
在阅读源码前,可以先阅读此文来了解框架大致思维
简介
EasyReact 是美团开源的一套响应式编程开发框架。在已有的 ReactiveCocoa 里,iOS 开发上手响应式编程是一个挑战,主要原因如下:
- 高学习门槛
- 易出错
- 调试困难
- 风格不统一
EasyReact 的出现就是为了降低 iOS 的响应式编程开发难度,让 MVVM 在 iOS 开发中变得更加优雅。
类与协议
在整个框架中共有两大数据结构:节点、边。
EasyReact 定义不同的节点和不同的边,用边将节点连接在一起,形成节点链,在这形成不同结构的节点链中进行数据传递。
由于节点和节点之间是依靠边来传递数据,而边传递的数据是单向的,所以便有了数据流的上下游概念
节点链
节点
节点主要工作是接收上游的边的数据并存储,同时传递给下游的边。
上游边和下游边
上游
节点的上游一定是边,上游边是节点数据输入。一个节点可以有多个上游边,也就是有多个数据输入口。
节点会强持有所有上游边。
下游
节点的下游一定是边,下游边是节点数据输出。一个节点可以有多个下游边,也就是多个数据输出口。
节点会弱持有所有下游边。
多个上游边和多个下游边
存储数据
每个节点都有一个value属性,存储当前节点的数据。可以手动调用- setValue:方法来设置值,也可以绑定上游边,从上游自动获取新的值。
不论是通过手动赋值,还是从上游自动得到的新的值,节点都会将该值传递给所有的下游边,由下游边自己传递给接下来的节点链。
定制
EasyReact 没有提供,也不推荐定制节点。
边
边主要工作是接收上游的节点的数据,并传递给下游的节点。
边不会存储上游的数据,但是边可以对数据进行变换,然后传递给下游节点。
上游
边的上游一定是节点,并且一个边有且只有一个上游节点,也就是只有一个数据入口。
边会强持有上游节点。
下游
边分为两大类,一个是监听边,另一个是变换边。
变换边是节点和节点之间数据传递时的边。变换边有且只有一个下游节点,也就是只有一个数据出口。变换边会弱持有下游节点。
监听边是整个节点链的末端边,主要负责数据流的最后一步数据处理,一般是UIView或者ViewModel的赋值工作。所以监听边没有下游节点。监听边有一个依赖的监听对象(id)listener,listener 会通过关联对象强持有监听边,也就是监听边的生命周期受listenr控制。
变换
数据在传递的过程中,可以在边上进行一次单一变换。不同变换对应的变换边都继承一个基类,并重写接收数据方法,在方法中对数据进行变换。一个子类就只能做一次单一变换。
目前已有的变换包括:变化、延迟、直到变化、过滤、降阶、搜索、跳过前几、取前几、限流
变换组
支持多个数据进行数学计算得到一个数据得变换,但是这种变幻是多个数据入口,一个数据出口的变换。所以使用变换组来管理这些数据入口。
边组的持有关系和数据流向
在建立下游节点时,会对应建立和上游节点个数一样的变换边,同时建立一个变换组。每一个边都会强持有变换组,变换组会弱持有每一个变换边。
其数据流向如下:
① 某个节点得到数据传递给边1,边1存储了数据,并告诉组数据有更新,但是组的计算需要两个数据,所以啥都不干
② 另一个节点得到数据传递给边2,边2存储了数据。
③ 边2告诉组,数据更新。
④ ⑤ 组向所有的边索取数据后,计算出了新的数据
⑥ 组将计算得到的数据传递给当前边(也就是边2)
⑦ 边2将数据直接发送给下一个节点
定制
EasyReact 提供现有变换边类,可以根据已有的类作为模板进行变换定制和扩展。
类图
由于节点和边都有接收数据的能力,所以在框架中定义了一个 EZRNextReceiver 的 protocol 来统一接口。
同时 边 又区分变换边和监听边(变换组属于变换边里),所以又给边定义了两套协议:EZRTransformEdge、EZRListenEdge
大致实现如下图:
类图
持有关系
不管是边还是节点,总结的持有关系如下:
下游的对象强持有上游的对象,上游的对象弱持有下游的对象。
下游对象一旦销毁,就会顺着持有链向上游依次释放对象,减少上游链的引用计数器
数据传递链
EasyReact 在传递数据的同时,还传递了一个上游节点链数组,以及一个对象作为上下文。
节点链数组
该数组是存放该数据已经经过的上游节点。每个节点在收到数据时,会在数组末尾加入节点自己,并将数据和新的数组传入到下游。
节点数组主要是防止闭环节点导致数据死循环的问题。节点在收到数据时,可以先判断数组中是否有自身,如果有,则说明该数据已处理过,应当停止处理此次流动。如果没有,则说明该数据在此节点没有处理过,需要保存并流向下游。
与 RAC 相比,节点链数组有效的防止了闭环导致的数据流死循环的错误。
上下文
上下文是一个任意类型的对象。在最上游的节点收到新的数据时,会生成一个新数据事件的上下文(意思是标记该次数据变化产生的源头、原因等等),上下文会随着数据在节点链中传输。
节点可以收到上下文,并截获,产生新的上下文对象传入下游(但是一般不会这么做)。
变换边在收到上下文时,可以通过上下文判断数据源头,并做不同的处理。
上下文在两个节点之间双向同步数据起到重要作用。如果要同步两个节点的数据,则需要在两个节点之间建立两个不同方向的边。为了防止重复设置值的问题,可以通过上下文来判断数据变化的源头来决定本次是否赋值。
下图为数据、节点链、上下文的流动图(数据从节点 F 开始流动,并在 B 中停止流动)。
闭环节点的数据流
已知缺陷
节点闭环导致的循环引用
虽然节点链解决了数据流循环的问题,闭环节点导致绝对上下游变成了相对上下游,形成对象的循环引用。
美团官方允许闭环节点的情况产生,但注明了需要开发者自己手动维护闭环节点的生命周期,要在某些时刻主动断开某一条边来破坏循环引用。
空容器内存占用
EasyReact 内部大量用到了弱引用容器,每一个上游对象都通过强持有弱引用容器来间接弱持有下游对象。
虽然并不会影响下游对象的生命周期,但是下游对象一旦销毁,该弱引用容器不会自动从上游对象中移除(除非上游对象也销毁)。
所以有可能在 App 运行期间,内存中存在大量这种空的弱引用容器。
与 RAC 的优劣分析
优势
- 四倍的性能
美团官方对 EasyReact 和 RAC 做性能比较。发现在同样功能、同样机型下,RAC 的性能消耗是 EasyReact 的4倍以上,最高达到7.25倍
性能检测
- 学习成本低
EasyReact 有完整的中文官方文档,代码量也比 RAC 少了很多。非常容易学习。
- 内部防止数据流闭环
EasyReact 已通过节点数组防止数据流闭环的问题,不需要开发者实现。
- 支持传递上下文和节点列表
EasyReact 在传递数据时会附加参数,便于下游对象对数据得不同处理。
劣势
- 没有常用标准节点
RAC 实现了一套 UIKit 常用信号,开发者可以直接用。EasyReact 并没有现成的 UIKit 节点实现,只有 KVO 的节点,不实用。
- 不支持上游销毁下游节点
RAC 支持上行信号执行到期时间,来关闭下行订阅者的订阅。
EasyReact 统一使用下游对象持有上游对象,上游对象的生命周期依赖下游对象,无法独立销毁。
为了解决这个问题,必须保证:
- 监听者应当直接持有被监听者
- 监听者早于被监听者销毁,或者同步销毁
在 iOS MVVM 架构中,View 和 ViewModel 刚好满足上述两条要求。不管是 View 和 ViewModel 之间进行数据绑定,还是 ViewModel 内部做代码精简,都可以正常使用 EasyReact。
未来
EasyReact 提供了一套完美的,已节点为基础的数据流结构,完成了 iOS 平台上的响应式编程。但是在 UI 层并没有提供标准模板给开发者用,使得原本很牛逼的技术却变得很难上手实用。
美团官方已公开 Swift 版本的 EasyReact 即将开源,同时搭载 EasyReact 的 MVVM 架构也将要开源。我们上手所需要的 UIKit 扩展工具都在这个 MVVM Demo 中。可以期待一下。
总结
EasyReact 确实是一个容易学习,轻量级,高性能的响应式编程框架,在此基础上和 RAC 比较能甩 RAC 好几条街。但是最实用的功能还没开源出来,这使得 EasyReact 实用性大大降低。
但是既然美团已经决定开源 MVVM 架构,那就说明这个缺陷有被填补的可能。
所以可以等到 MVVM 架构开源后,再上手 EasyReact。