[iOS] MVC、Flux、Redux在iOS中的使用

首先声明这一篇完全是根据同事的视频学习的笔记哈~~

MVC

由于其实MVVM、MVP都是基于MVC,所以先看MVC的组成:它主要是抽象出model和view,通过双向绑定,让view可以根据model的状态进行更新

比如发生了一次view的点击,其实这个点击会先给controller,然后controller把动作给model,让model去做事情,然后view会监听model的变化。

MVC

MVC的优点:
实现视图层和业务层的分离,一定程度上降低耦合性
降低耦合性能够提高代码可维护性,增强协作的可能性
提高复用性,例如一个model对应多个view

当controller里面的view越来越多,model也会越来越多,如果一个model被多个view监听,这个view可能🈶引发了新的model修改,然后整个数据流就会很混乱:


MVC的缺点

MVC的缺点:
没有明确模块间的依赖关系和调用方式,导致没有真正的分离
没有明确的状态同步的方式,数据流复杂

所以MVC对于复杂大型项目不容易拓展,于是延伸出了Flux架构。


Flux

它的主要特点就是单向数据流:


flux

Action其实是需要触发状态变更的一个动作,而非仅限于用户操作哦~ 一个完整的数据流是酱紫的:例如一个view点击,会发一个action携带点击相关的参数给dispatcher,dispatcher将动作给store,store去做一些操作影响状态的变更,变更的时候会发一个change事件,让view去更新界面。

下面看一个iOS上面的实现:

#import <Foundation/Foundation.h>
#import "FAction.h"

NS_ASSUME_NONNULL_BEGIN

@interface FStore : NSObject

@property (nonatomic, strong, readonly) id state;

-(void)registerForActionClass:(Class)actionClass observer:(id)observer;
-(void)unregisterForActionClass:(Class)actionClass observer:(id)observer;

-(BOOL)responseToAction:(FAction *)action;
-(void)receiveAction:(FAction *)action;

@end

NS_ASSUME_NONNULL_END

store的职责主要是 ① 维护状态state;② 提供注册和注销observer的方法;③ 接收action回调给observer处理

它的实现大概是酱紫的:

#import "FStore.h"

@interface FStore()

@property (nonatomic, strong) NSMapTable *observers;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation FStore

- (instancetype)init {
    if (self = [super init]) {
        _semaphore = dispatch_semaphore_create(1);
        _observers = [NSMapTable strongToStrongObjectsMapTable];
    }
    return self;
}

-(void)registerForActionClass:(Class)actionClass observer:(id)observer {
    dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    id actionObservers = [self.observers objectForKey:NSStringFromClass(actionClass)];
    
    NSHashTable *actionObserversTable;
    if (actionObservers != nil && [actionObservers isKindOfClass:[NSHashTable class]]) {
        actionObserversTable = (NSHashTable *)actionObservers;
    } else {
        actionObserversTable = [NSHashTable weakObjectsHashTable];
    }
    
    [actionObserversTable addObject:observer];
    [self.observers setObject:actionObserversTable forKey:NSStringFromClass(actionClass)];
    dispatch_semaphore_signal(self.semaphore);
}

-(void)unregisterForActionClass:(Class)actionClass observer:(id)observer {
    dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    id actionObservers = [self.observers objectForKey:NSStringFromClass(actionClass)];
    
    if (actionObservers == nil || ![actionObservers isKindOfClass:[NSHashTable class]]) {
        dispatch_semaphore_signal(self.semaphore);
        return;
    }
    
    NSHashTable *actionObserversTable = (NSHashTable *)actionObservers;
    if (![actionObserversTable containsObject:observer]) {
        dispatch_semaphore_signal(self.semaphore);
        return;
    }
    [actionObserversTable removeObject:observer];
    [self.observers setObject:actionObserversTable forKey:NSStringFromClass(actionClass)];
    dispatch_semaphore_signal(self.semaphore);
}

-(BOOL)responseToAction:(FAction *)action {
    dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    id actionObservers = [self.observers objectForKey:NSStringFromClass([action class])];
    
    if (actionObservers == nil || ![actionObservers isKindOfClass:[NSHashTable class]]) {
        dispatch_semaphore_signal(self.semaphore);
        return NO;
    }
    
    NSHashTable *actionObserversTable = (NSHashTable *)actionObservers;
    BOOL shouldResponse = actionObserversTable.count > 0;
    
    dispatch_semaphore_signal(self.semaphore);
    return shouldResponse;
}

-(void)receiveAction:(FAction *)action {
    if (![self responseToAction:action]) {
        return;
    }
    
    dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    id actionObservers = [self.observers objectForKey:NSStringFromClass([action class])];
       
    if (actionObservers == nil || ![actionObservers isKindOfClass:[NSHashTable class]]) {
        dispatch_semaphore_signal(self.semaphore);
       return;
    }
    
    NSHashTable *actionObserversTable = (NSHashTable *)actionObservers;
    [[actionObserversTable objectEnumerator].allObjects enumerateObjectsUsingBlock:^(id  _Nonnull observer, NSUInteger idx, BOOL * _Nonnull stop) {
        // 让observer处理action
    }];
    
    dispatch_semaphore_signal(self.semaphore);
}

@end

Action就是被传递的动作啦,它主要由两部分组成:类型 & 附带的数据,例如点击action可以附带一下被点击的信息之类的:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FAction : NSObject

@property (nonatomic, readonly, assign) NSInteger actionType;
@property (nonatomic, readonly, strong) id payload;

- (instancetype)initWithActionType:(NSInteger)actionType payload:(id)payload;

@end

NS_ASSUME_NONNULL_END

==========================

#import "FAction.h"

@implementation FAction

- (instancetype)initWithActionType:(NSInteger)actionType payload:(id)payload {
    if (self = [super init]) {
        _actionType = actionType;
        _payload = payload;
    }
    return self;
}

@end

type一般就是枚举,举个例子~ 例如type是购买,payload是购买数量。


dispatcher主要是动作的分发,将动作转发给对应的store。所以他的职责主要是:① 注册store;② 注销store;③ 将action给store

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class FAction, FStore;

@interface FDispatcher : NSObject

-(void)dispatchAction:(FAction *)action;

-(void)registerStore:(FStore *)store;
-(void)unregisterStore:(FStore *)store;

@end

NS_ASSUME_NONNULL_END

=================================

#import "FDispatcher.h"
#import "FAction.h"
#import "FStore.h"

@interface FDispatcher()

@property (nonatomic, strong) NSMutableSet<FStore *> *stores;

@end

@implementation FDispatcher

- (instancetype)init {
    if (self = [super init]) {
        _stores = [NSMutableSet set];
    }
    return self;
}

-(void)dispatchAction:(FAction *)action {
    [self.stores enumerateObjectsUsingBlock:^(FStore *  _Nonnull store, BOOL * _Nonnull stop) {
        [store receiveAction:action];
    }];
}

-(void)registerStore:(FStore *)store {
    [self.stores addObject:store];
}

-(void)unregisterStore:(FStore *)store {
    if ([self.stores containsObject:store]) {
        [self.stores removeObject:store];
    }
}

@end

Summary - flux
  • 实现了View -> Dispatcher -> Store -> View 的单向数据流
  • 本身是允许多个store的,但是这样又会有多个数据源的状态同步等问题,所以推荐一个store
  • Action是唯一可以造成状态变化的方式,比MVC各种方式都可以触发操作要更明确
  • dispatcher全局唯一,对于单store的情况比较多余

Redux

Redux

store仍旧维护状态,和flux的区别就是规定整个app只有一个store,action仍旧是动作;不一样的是引入了一个处理同步数据流的reducer,以及处理异步数据流的middleware

例如,操作view的时候也会发出一个action,由于只有一个store,所以可以直接给store,然后store会把动作交给reducer,reducer根据action以及旧的state生成新的state返回给store,store中的state就会更新,也就会通知view去更新显示。

reducer就是一个纯函数,输入就是oldState以及action,然后输出就是一个newState,注意这里就类似函数式编程,reducer是不应该改动到对象的属性的,就是不应该产生副作用,仅仅是用于计算的感觉。也就是无论执行多少次,结果都一样,无论对象是什么,只要输入的一致,输出就不变。

所以reducer不可以修改传入的参数;不能执行有副作用的操作例如调用API请求(每次返回的结果可能不同);也不能调用非纯函数。

middleware是处理异步工作流的,例如点击按钮以后会请求API,区别是middleware是没有返回的:

middleware的点赞流程

redux的重要原则:

  1. 单一数据源store,避免多状态同步的问题
  2. state是只读的(这里的理解猜测是:也就是不immutable的,reducer会返回一个新的state对象而非在原有的state上面做修改)
  3. 引入纯函数的reducer,不会带有副作用
flux 和 redux 的区别

state作为维护状态的对象的基类,没有什么属性,不同state只要继承基类即可,然后添加自己的属性:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RState : NSObject

@end

NS_ASSUME_NONNULL_END

store的主要职责为:① 维护state;② dispatch action;③ 注册/取消注册监听

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class RState, RAction, RReducer, RMiddleware;

@protocol RStoreObserver <NSObject>

-(void)onStateChanged:(RState *)newState;

@end

@interface RStore : NSObject

@property (nonatomic, strong, readonly) RState *state;

-(instancetype)initWithReducer:(RReducer *)reducer;
-(instancetype)initWithReducer:(RReducer *)reducer middleware:(nullable RMiddleware *)middleware;

-(void)registerObserver:(id<RStoreObserver>)observer;
-(void)unregisterObserver:(id<RStoreObserver>)observer;

-(void)dispatchAction:(RAction *)action;

@end

NS_ASSUME_NONNULL_END

它的实现就酱紫:

#import "RStore.h"
#import "RState.h"
#import "RAction.h"
#import "RReducer.h"
#import "RMiddleware.h"

@interface RStore()

@property (nonatomic, strong) RState *state;
@property (nonatomic, strong) RReducer *reducer;
@property (nonatomic, strong) RMiddleware *middleware;
@property (nonatomic, strong) NSHashTable *observers;

@end

@implementation RStore

-(instancetype)initWithReducer:(RReducer *)reducer {
    return [self initWithReducer:reducer middleware:nil];
}

-(instancetype)initWithReducer:(RReducer *)reducer middleware:(nullable RMiddleware *)middleware {
    if (self = [super init]) {
        _reducer = reducer;
        _middleware = middleware;
        _observers = [NSHashTable weakObjectsHashTable];
    }
    return self;
}

-(void)registerObserver:(id<RStoreObserver>)observer {
    if (observer == nil || [self.observers containsObject:observer]) {
        return;
    }
    [self.observers addObject:observer];
}

-(void)unregisterObserver:(id<RStoreObserver>)observer {
    if (observer == nil) {
        return;
    }
    
    if ([self.observers containsObject:observer]) {
        [self.observers removeObject:observer];
    }
}

-(void)dispatchAction:(RAction *)action {
    if (action.shouldMiddlewareHandle && self.middleware != nil) {
        [self.middleware handleAction:action state:self.state];
        return;
    }
    RState *newState = [self.reducer handleAction:action state:self.state];
    self.state = newState;
}

- (void)setState:(RState *)state {
    _state = state;
    [self.observers.objectEnumerator.allObjects enumerateObjectsUsingBlock:^(id<RStoreObserver>  _Nonnull observer, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([observer respondsToSelector:@selector(onStateChanged:)]) {
            [observer onStateChanged:state];
        }
    }];
}

@end

注意哦,这里是给了store一个reducer和一个middleware,然后reducer里面可以根据处理的action的class来区分如何处理,或者让它内部维护的reducer array来处理;当然你也可以在store里面维护一个dict,记录action对应的reducer,这样就要存很多reducer而已。


action在这里会多一个属性,是不是需要middleware先handle,store会根据这个属性来决定把action给reducer还是middleware,其他两个属性类似flux:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RAction : NSObject

@property (nonatomic, assign) BOOL shouldMiddlewareHandle;

@property (nonatomic, readonly, assign) NSInteger actionType;
@property (nonatomic, readonly, strong) id payload;

@end

NS_ASSUME_NONNULL_END

reducer你可以定义一个协议,也可以定义基类,注意handle是一个纯函数:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class RAction, RState, RStore;

@interface RReducer : NSObject

@property (nonatomic, weak) RStore *store;
-(RState *)handleAction:(RAction *)action state:(RState *)state;

@end

NS_ASSUME_NONNULL_END

middleware和reducer非常类似,只是handle action的时候没有返回值:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class RAction, RState, RStore;

@interface RMiddleware : NSObject

@property (nonatomic, weak) RStore *store;
-(void)handleAction:(RAction *)action state:(RState *)state;

@end

NS_ASSUME_NONNULL_END

middleware在处理action里面可以先做异步动作,然后当异步过程结束以后,再调用store的dispatch action,传入新的action,由于这里可能会用到store,所以其实middleware和reducer是可以持有store哒~ 如果使用注意不要循环引用即可


举个例子,比如点击动态的点赞,按钮会变成选中状态,并且点赞数+1:

  • 创建store,调用点赞的middleware以及reducer的handle action
  • action需要定义一个type,这里就是点赞type,并携带一个其他数据属性也就是动态的model
  • store分发action给middleware,middleware会根据action type进行不同处理,这里会先取动态的model去远端请求点赞api
  • middleware的请求回来以后会把action的shouldMiddlewareHandle设为NO以后传递给store,store再去分发这个action给reducer
  • reducer收到action以后也是通过type找到处理方式,然后修改state里面的属性(state.detailModel.hasDigg = YES),返回新的state(这里没有创建新的对象,但严格意义应该创建新的)
  • store会设置新的state,触发observers的state change监听,view在这里把点赞按钮设为选中。

什么时候用redux
  1. 需要单一数据源
  2. 状态较多
  3. 视图层级or数目多

Discussion

Finally,用个人角度以及和同事聊天的感受来探讨一下这几种模式叭,MVX系列(MVC / MVP / MVVM)和Flux其实都只是形式或者说实现上面的不一样,如果MVC会造成绑定混乱,例如按理说,无论是VC绑定还是VM和V绑定,其实都应该是一对一,但是经常我们不能保证这一点,毕竟项目很庞大。

Flux 和 Redux 很重点的一个事儿是:对状态进行梳理。也就是尽量减少那种 VM 一对多的关系所造成的的连锁反应。并且,它更容易测试以及理解的一点在于,监听状态改变后做相应的操作,这一点相比MVX系列更清晰。但是本质上,还是可能会造成连锁反应,只是它更强调有限的状态,让系统更可控的感觉。

所以归根结底,好的设计模式其实不仅仅是说实现上,更重要的是我们如何避免耦合,或者说让耦合能做到泾渭分明。这个是根本,如果可以做到其实用 MVX 和 flux 区别不会特别大,但是 flux 仍旧是一个很好的模式,因为它很清晰有条理,强迫我们去梳理出一个个独立的状态形成单向数据流,尽量减少不可控的连锁反应。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354