通俗易懂圖解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地址
相關文章
- Vue、MVVM、MVC、雙向繫結VueMVVMMVC
- vue雙向繫結的原理及實現雙向繫結MVVM原始碼分析VueMVVM原始碼
- 剖析Vue原理&實現雙向繫結MVVMVueMVVM
- WPF之AvalonEdit實現MVVM雙向繫結MVVM
- MVVM雙向繫結機制的原理和程式碼實現MVVM
- mvvm-simple雙向繫結簡單實現MVVM
- vue 雙向繫結(v-model 雙向繫結、.sync 雙向繫結、.sync 傳物件)Vue物件
- Databinding 雙向繫結詳解
- 通俗易懂講解並手寫一個vue資料雙向繫結案例Vue
- Vue雙向繫結初探Vue
- vue雙向繫結原理Vue
- vue雙向繫結盲區Vue
- JS雙向資料繫結JS
- Vue雙向繫結實現Vue
- 揭密 Vue 的雙向繫結Vue
- javascript中的雙向繫結JavaScript
- Vue雙向繫結原理,教你一步一步實現雙向繫結Vue
- 從單向到雙向資料繫結
- Vue資料雙向繫結原理Vue
- vue生命週期、雙向繫結Vue
- vue雙向資料繫結原理Vue
- vue實現prop雙向繫結Vue
- vue實踐:元件雙向繫結Vue元件
- angular雙向繫結—(按鈕+下拉)Angular
- 原生js雙向資料繫結JS
- [JS] 資料雙向繫結原理JS
- Flex4中雙向繫結Flex
- vue v-model 雙向繫結Vue
- 【.NET6+WPF】WPF使用prism框架+Unity IOC容器實現MVVM雙向繫結和依賴注入框架UnityMVVM依賴注入
- 這一次 徹底搞懂Vue針對陣列和雙向繫結(MVVM)的處理方式Vue陣列MVVM
- Js深度拷貝解決雙向繫結問題JS
- 簡單易懂的雙向資料繫結解讀
- javascript實現資料的雙向繫結(手動繫結)JavaScript
- 簡要實現vue雙向繫結Vue
- vue-原始碼剖析-雙向繫結Vue原始碼
- 雙向資料繫結實現原理
- angular雙向繫結簡單實現Angular
- 雙向繫結的極簡實現