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。