通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)

Posted Deft_MKJing宓珂璟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)相关的知识,希望对你有一定的参考价值。

前言

一个前辈的MVVM介绍
其实MVVM就是MVC的进化版本,相对于臃肿的Controller,代码越来越多之后,有一部分人就用了新的设计模式,其实看久了也没什么,通俗点讲,其实就是把之前Controller里面的代码逻辑全部移植到了ViewModel里面,相对于以前而言,控制器也被归属于View一类,那么他和View一样都会有自己的ViewModel去处理逻辑,而且ViewModel必然拥有Model,这样的关系使得控制器代码会减少很多很多,处理起来又多了一个类,本身设计模式里面有代理,通知,KVO等,不同业务对应不同的设计模式,个人理解为了减少控制器的代码,引进了新的类,那么类的交互就变得更麻烦了,因此RAC出现了,他帮我们直接管理了苹果的那一套数据处理设计模式,统一用它的”信号流”来进行,谁用谁知道啊。。。。。。

双向绑定
1.Model—->View 这种流向很简单,你请求数据之后,通过Block的回调,最终更新UI
2.View—–>Model 反向绑定也一样,View触发事件,更新对面ViewModel里面绑定的数据源,例如登录注册的Textfield,你输入和删除的时候,你的Model字段会对应更新,当你提交的时候,读取ViewModel的字段,就是已经更新的最新数据。这是一种方式,我个人感觉如下图的另一种更容易理解,比如你选择某个cell或者点赞的时候,View事件触发,更新绑定的ViewModel字段,拥有ViewModel的控制器,用RACObserve来进行该字段开关的读取,如果监听到YES,就刷新对应的页面UI

简单看下自己理解的MVVM

效果图



图片和文字闪烁的效果传送门
闪烁效果

RAC的第一种流程介绍—>RACSignal

网上很多基本的介绍,这里主要讲一下流程
1.如果用RACSignal来创建信号(内部Block有发送信号以及取消信号的回调,为什么是3和4呢,原因在后面)

// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) 
// 3.发送信号
[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];
// 4.取消信号,如果信号想要被取消,就必须返回一个RACDisposable
// 当信号被手动或者自动取消订阅之后会回调到这里,进行一些资源的释放
return [RACDisposable disposableWithBlock:^
NSLog(@"取消订阅");
];
];

注意:上面创建的方法内部代码主要归结于创建一个集成于RACSignal的子类—>RACDynamicSignal,然后
通过静态方法实例化出来,并把传进去的任务Block进行对象属性的存储

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe 
    RACDynamicSignal *signal = [[self alloc] init];
    signal->_didSubscribe = [didSubscribe copy];
    return [signal setNameWithFormat:@"+createSignal:"];

2.创建的信号RACSignal(子类RACDynamicSignal)来调用第二步[Signale subscribeNext:^];来进行信号的订阅,内部转换代码

RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
    return [self subscribe:o];

注意:这里RAC的设计者向我们隐藏了RACSubscriber,对外暴露了RACSignal,所有的内部工作都由RACSubsriber进行完成传递
如果你再点进去,这里传递的Signal,会判断之前创建的时候传进的Block是否为空,如果有任务,直接回调Block

RACDisposable *innerDisposable = self.didSubscribe(subscriber);

3.现在有人订阅了,又回调了Block,然后触发任务,完成之后调用

[subscriber sendNext:@"mkj"];
[subscriber sendCompleted];

// 内部代码最终调用方法如下
- (void)sendNext:(id)value 
    @synchronized (self) 
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;

        nextBlock(value);
    

最终回调到订阅的时候NextBlock的任务

4.这个可有可无,返回一个RACDisposable,对订阅取消进行资源的释放

总结:把他比喻成工厂,当你需要打开生产流水线的时候(创建信号,带有任务),这个时候你工人都没有,根本不会走你的任务
信号不会被传递,而你有工人来的时候(就是订阅了信号),这个时候流水线才开始进行加工,这就是个人理解的冷信号模式
也可以把冷信号理解为未被订阅的信号,理解为信号里面的任务是不会进行的,只有订阅者的加入,信号才会变成热信号
也就是这玩意需要手动开启信号

RAC第二种的流程介绍—>RACSubject(继承与RACSignal)

1.创建信号
RACSubject *subject = [RACSubject subject];
该方法和上面的创建方式有所不同,他的实例化出来之后只是创建了一个数组,专门用来存储订阅信号的订阅者


2.订阅信号

[subject subscribeNext:^(id x) 
// 当信号sendNext的时候会回调
NSLog(@"%@",x);
];
// 这方法也是和上面的有所区别,RACSubject该对象会把订阅者放到之前创建的数组里面,然后啥都不做了

3.[subject sendNext:value];

内部代码
[self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) 
        [subscriber sendNext:value];
    ];
可以看出,当他调用sendNext的时候,是会进行数组的遍历,然后挨个对订阅者发送消息

总结:其实这就是所谓的热信号模式,还是拿工厂来做比喻,RACSubject是不管你有没有人订阅,我工厂24小时开启流水线
我管你有没有人加工,有人来了,我就用数组登记一下,信号来了的时候你们就负责接收任务,没人的时候我还是就好比我的员工
花名册是空的,但是照样生产,只是没人做事罢了,那么这里的RAC信号流就是没人处理罢了,会被丢弃

知识点:区别RACSubject和RACSignal
1.我个人理解,前者是冷信号模式,需要有人订阅才开启热信号,后者是热信号默认,不管你有没有订阅

2.前者其实是一旦有人订阅,就会去做对应的一组信号任务,然后进行回调,可以理解为有人的时候任务启动,没人的时候挂机
没错,我是把它简单理解为代理,后者是热信号,信号负责收集订阅者数组,发信号的时候回遍历订阅者,一个个执行任务
你可以把它理解为通知,我管你有没有接收,我照样发送,没人就丢弃

3.前者个人用来进行网络请求,后者进行类似代理或者通知的数据传递模式,这样就可以简单的理解为,RAC其实就是把apple的一套
delegate,Notification,KVO等一系列方法综合起来了,用起来更舒服罢了

4.那么MVVM模式下,本身就多了个ViewModel,交互起来需要更多的设计模式协助,RAC就解决了这个问题,直接用这个设计模式来搞
数据传递和监听的代码就清晰很多了


既然已经了解了RAC的流程,Demo走起!!!

MVVM + RAC示例Demo

1.用到了网络请求的信号传递

2.用RACObserve宏进行属性的KVO观察

3.用RACSubject进行数据的回调

4.用RACSequence进行异步数组和字典(打印的是RACTuple)的遍历

5.RAC–>combineLatest的方法进行简单多输入框登录注册页面模拟


先看下Demo里面各个类的关系


1.首先看下用MVVM的基类(MKJBaseViewController)

// baseVC的基础ViewModel
// 子类重写就能覆盖类型
@property (nonatomic,strong,readonly) MKJBaseViewModel *viewModel;

/**
 唯一初始化方法

 @param viewModel 传入ViewModel
 @return 实例化控制器对象
 */
- (instancetype)initWithViewModel:(MKJBaseViewModel *)viewModel;

/**
 布局UI 子类重写
 */
- (void)setupLayout;

/**
 请求网络数据 绑定数据 子类重写
 */
- (void)setupBinding;

/**
 设置数据回调,点击事件处理  子类重写
 */
- (void)setupData;

初始化的时候
MKJDemoViewModel *viewModel = [[MKJDemoViewModel alloc] init];
            MKJDemoViewController *demoVC = [[MKJDemoViewController alloc] initWithViewModel:viewModel];
            [self.navigationController pushViewController:demoVC animated:YES];

2.再看一下TableViewController的基类
基础的实现和普通拥有tableView的控制器一样,无非区别在于代理的逻辑和数据交给了ViewModel

// 交给子类实现,传递最终的cell类
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath

    @throw [NSException exceptionWithName:@"抽象方法未实现"
                                   reason:[NSString stringWithFormat:@"%@ 必须实现抽象方法 %@",[self class],NSStringFromSelector(_cmd)]
                                 userInfo:nil];



#pragma mark - tableView datasource
// 交给ViewModel去实现
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

    return [self.viewModel numberOfSections];


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

    return [self.viewModel numberOfRowInSection:section];


- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section

    return [self.viewModel heightForHeaderInSection:section];


- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

    return [self.viewModel viewForHeaderInSection:section];


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

    MKJBaseTableViewCell *cell = [[self cellClassForRowAtIndexPath:indexPath] cellForTableView:tableView viewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
    cell.selectionStyle = [self.viewModel tableViewCellSelectionStyle];
    return cell;


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

    CGFloat height = tableView.rowHeight;
    NSNumber *calculateHeight = [[self cellClassForRowAtIndexPath:indexPath] calculateRowHeightWithViewModel:[self.viewModel cellViewModelForRowAtIndexPath:indexPath]];
    if (calculateHeight) 
        height = calculateHeight.floatValue;
    
    return height;

3.最终上层的ViewController核心代码,最终剩下就这么点代码了,瘦身不。。。。

// 基本布局代码,顺便设置个RACObserve
- (void)setupLayout

    [super setupLayout];
    @weakify(self);
    [RACObserve(self.viewModel, isNeedRefresh) subscribeNext:^(id x) 
        @strongify(self);
        if ([x boolValue]) 
            [self.tableView reloadData];
        
    ];


// ViewModel进行网络请求
- (void)setupBinding

    [super setupBinding];
    @weakify(self)
    [self.viewModel sendRequest:^(id entity) 
        @strongify(self);
        [self hideLoadingViewFooter];
        [self.tableView reloadData];

     failure:^(NSUInteger errCode, NSString *errorMsg) 

    ];

// 返回对应的CellClass
- (Class)cellClassForRowAtIndexPath:(NSIndexPath *)indexPath

    return [MKJDemoTableViewCell class];

4.来看下ViewModel在做什么,核心还是网络请求,RAC信号流的网络请求

// 网络请求外部
- (void)sendRequest:(MKJRequestSucceed)succeedBlock failure:(MKJRequestFailure)failBlock

    [[self.model requestDemoDatasWithPage:[self.currentPage integerValue] maxTime:self.currentMaxTime] subscribeNext:^(id data) 
        if (data) 
            self.entity = data;
            [self handlePagingEntities:self.entity.list
                            totalCount:@(self.entity.info.count)
                    cellViewModelClass:[MKJDemoTableViewCellViewModel class]
                               maxTime:self.entity.info.maxtime];
        
        !succeedBlock ? : succeedBlock(data);
    ];


// 网络请求内部
- (RACSignal *)getRequestWithURLString:(NSString *)URLString
                  parametersDictionary:(NSDictionary *)paraterDictionary
                     parserEntityClass:(Class)parseEntityClass

    // 根据异步请求创建一个新的RACSinal
    @weakify(self)
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) 
        @strongify(self);

        [self.httpHelper getRequestWithUrlString:URLString
                             parametersDictonary:paraterDictionary
                                     entityClass:parseEntityClass
                                   completeBlock:^(id data) 
                                       [subscriber sendNext:data];
                                       [subscriber sendCompleted];
        ];

        return nil;
    ];


根据之前上面介绍的RAC逻辑,外部注册订阅者的时候成为热信号,然后调用创建信
号的Block,完成网络请求之后sendNext进行回调,设计如此,然后在ViewModel中
把Model组装完毕,进行外部的TableView reload。然后再次调用代理方法的时候,
会再次进到ViewModel里面获取已经组装好的数据返回给TableView的DataSource
,OK了

5.Cell也单独配置了对应CellViewModel,就是在RAC网络请求回来之后,把实体Model,用CellViewModel来进行组装,只是把之前装数据Model的数组,用来装拥有数据模型的CellViewModel,明白一点,ViewModel拥有Model,就能搞定数据的逻辑处理





我TM的终于明白了,NO BB Show me the code了,这根本说不清楚,需要的同学还是直接看Demo吧,最终的逻辑转换就是上面的MVVM效果图,理解了就可以了,无非就是一种设计思想,不过加上RAC确实还不错。。。

我个人也比较喜欢看Demo,需要的还是直接开撸吧
正确Demo地址

以上是关于通俗易懂图解MVVM和RAC双向绑定介绍(附Demo)的主要内容,如果未能解决你的问题,请参考以下文章

RAC+MVVM,不同VC(VM)间数据双向绑定

vue数据双向绑定原理

vue数据双向绑定原理

vue源码解读1

通俗易懂!图解Go协程原理及实战

通俗易懂的图解机器学习之机器学习概论