• MVVM深入了解


    写在前面

    MVC,MVP,MVVM……移动端的开发可谓是在MVX的海洋中摸爬滚打!然而,V和M的概念不说,关于P,关于VM,它为什么叫Presenter,为什么叫ViewModel?我们实践中的P,VM所做的事情真的和它们的概念对得上么?

    本篇即基于MVVM在移动端的应用这一话题做一些简单的讨论,希望大家可以借以回顾自己搭过的框架,码过的代码,能唤起一些有趣更有意义的思考!

    第一篇:我对“MVVM”的初识

    那是在2015年的时候,MVVM被炒的火热,一日,面朝我已经尽心竭力做好了概念分组的“巨型VC”,我无力地将头专向小马哥,“编辑这部分代码我还是感到很难受……”

    //某VC中
    [aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
        [Error check code];
        NSDictionary *tmpDic = (NSDictionary *)respData;
        NSNumber *tmpNumber = [tmpDic objectForKey:@"boolVal"] ;  
        if (YES == [tmpNumber boolValue]) {
           _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
        } else {
           _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
        }}];
    

    如上,很常见的,网络请求后,错误检查,解析数据并用数据更新视图。
    “如此清晰顺畅无比的场景,还能如何优化呢?‘感到难受’,这也没办法啦,什么都不能做了!”我们有这样自我合理化的内心os太正常不过了,但多年的经验不断地让我印证一个真理:事出反常必有鬼(感觉不爽,必可优化)。果然!……

    “那是因为你把视图和逻辑耦合到一起了!”小马哥回答。

    Step1 可以将我们期望在回调中做的事情进行简单的概念分组

    //某VC中
    [aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
        /* Error check */
        /* Handle Parser */
        /* Update View */
    }];
    

    Step2 分别抽象出解析&更新视图的具体处理

    /* Handle Parser */
    - (NSString *)parserTitle:(NSDictionary *respData) {
        NSString *tmpStr = nil;
        NSNumber *tmpNumber = [respData objectForKey:@"boolVal"] ;  
        if (YES == [tmpNumber boolValue]) {
            tmpStr = [respData objectForKey:@"title"] ; 
        } else {
            tmpStr = [respData objectForKey:@"title2"] ;
        }}];
        return tmpStr;
    }
    
    /* Update View */
    - (void)updateViewWithTitle:(NSString *)title {
        _contentView.titleLable.text = title;
    }
    

    Step3 方法抽象封装,并新建文件(logicModel)来承载功能简单但实现复杂的逻辑代码

    //某VC中
    [_logicModel requestTitleInfoWithResponse:^(NSString *title) {
        /* Update View */
    }];
    
    //封装到logicModel文件中
    - (void)requestUserInfoWithResponse:(void(^)(id userInfo, NSError *error))callback {
        [aRequester sendPostReqWithUrl:(NSString *)url paras:(NSDictionary *)paras response:(id respData, NSError *error) {
            /* Error check */
            /* Handle parser */
            /* Callback */
            callback(title);
        }];
    }
    

    有什么不一样么?或许不经过实践操作中对代码维护效率精益求精地追求,很难通过上面的例子直观的理解抽象的好处。甚至会有一些浮躁的逆反心理作祟,认为这么做多此一举。

    抽象/封装不一定都是好的,它们的应用要权衡地考虑某个实现模块的复杂性,从而选择一个最合适的抽象层次。而抽象/封装的最基本原则参考,我想应当是:概念合理

    上面的例子即是简单地将“数据的处理”“视图的更新”相互独立起来,使视图的更新更为纯粹!简言之,我们期望避免下面场景的出现

        if (YES == [tmpNumber boolValue]) {
           _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
        } else {
           _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
        }
    

    然后,小马哥告诉我,这就是MVVM,即当前最流行的Model-View-ViewModel模式。

    我上面起名为LogicModel的文件即为小马哥所说的“VM”,它负责将网络请求、请求解析和一些数据处理逻辑进行封装,从而使VC变得“轻”一些

    想象一下我们的思维走向:
    1) 与视图无关的数据逻辑问题,直接往从VM入手排查
    2) 与视图展示有关的逻辑问题,在VC到View的流程中一看数据对接,二看VM反馈的数据是否有误。
    可谓是结点清晰,定位问题毫无压力!大赞!不愧是“MVVM”!

    可是,VM就是逻辑封装自然演化的一个“代号”么?

    VM = View Model,是视图的模型,“模型”一词,从概念上倾向于一种“静态”,而逻辑处理,网络请求,信号接收这些趋向于一种异步的“动态”,而且好像和“视图”的概念差别有些大。这种封装固然有它的优势所在,但MVVM的设计者干嘛对它起名为VM呢?视图的模型?叫LogicModel (逻辑模型),或者VCTool(VC工具)怎么都比VM合理吧?

    只是一个名字而已嘛!然而,就我对“大牛”的理解,他们对于某种概念的名称拟定,是绝不会马马虎虎了事的

    VM的本源必然就是一个VM!一个视图的模型!

    第二篇:追溯MVVM的提出

    或许是我对于VM的理解方向不对吧?毕竟从网上的众多文章的分析说明,从同事的实践中,我们对于MVVM又或MVP的在移动端的应用实践竟然出奇地一致!(如下图)

     
    image.png

    翻阅了几十篇相关的文章,每篇的说的颇有道理,很多文章还是分了上中下篇,并配以图示,似颇为系统的对MVC,MVP,MVVM进行介绍。不得不说,这些文章颇具指导意义,确实可以让很多限于逻辑耦合深渊的朋友找到一盏明灯,让他们的项目变得清晰而易于维护。但我还是任性地感觉,他们在打着MVVM的旗号在讲VC减负——我想要做的,是接近MVVM的本源

    MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并且应用在微软的软件开发中。我找到了那片博文,并进行了翻译和仔细的思考探究。

    《Model/View/ViewModel pattern for building WPF apps》
    John Gossman
    译文链接:
    https://www.jianshu.com/p/b0b80163782f
    原文链接: https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/

    这篇博文中,有这样这样两句有趣的话:

    1)Model/View/ViewModel is a variation of Model/View/Controller (MVC) that is tailored for modern UI development platforms where the View is the responsibility of a designer rather than a classic developer.(译:MVVM是MVC模式的一个演变,针对一个视图的展示样式,比起传统的开发者,现在往往是设计师更为关注,MVVM正是为这种状况而定制的一种模式。)

    2)The term means "Model of a View", and can be thought of as abstraction of the view。(译:它意为“视图的模型”,可以将它想象成一个抽像化的视图。)

    那么,基于对MVVM本源的解读,我们在一个“可以被想象成视图的抽象”VM中添加大量网络,页面跳转等逻辑显然不太合适了。(它是视图,它是视图,它是视图,请这样对自己洗脑!)同时,我们也想思考下关于“让设计师去完成视图展示”这个有趣的点。

    第三篇:MVVM基于WPF的应用(最初的应用场景)

    WPF即Windows Presentation Foundation,是微软推出的基于Windows 的用户界面框架。

    第二篇的译文中原作者举例的应用界面是Sparkle,我这边则以类似的OmniGraffle(一款原型绘制软件)的界面进行说明。(没什么特别的原因,因为我正在用OmniGraffle,更容易截图:)

     
    image.png

    如果大家阅读了第二篇提供的《Model/View/ViewModel pattern for building WPF apps》,你会发现文中举例的Sparkle界面操作栏和我举例的OmniGraffle界面圈红的部分是很相似的。那么,参照文中的VM划分方式,我们可以设计A部分对应一个ViewModel A(当然OmniGraffle不一定是这样实现的),B部分对应一个ViewModel B,然后B部分的“填充”,“笔画”,“阴影”,“形状”,“线条”亦可以分别对应5个小的ViewModel……(如下图)

     
    image.png

    一个有趣的点,View的层次叠加变成了VM的层次叠加!VM真如一个View的抽象一般!

    同时,第二篇摘录的另一段译文引导的另一个问题:什么叫设计师更关注UI的页面展示?不要小看我们UI同学哦,当下很多的设计师都有css、html的开发经验,同时MVVM由微软提出,记得微软有一个自己的XAML吧?它正式一种搭建UI的语言。所以,基于MVVM的模式,我们至少可以从概念上将视图完全剥离(甚至交给UI同学去渲染与实现),模型中只要有视图中需要展示的元素的具体内容数据即可,他不关心任何视图的布局,渲染效果。

    同事,针对视图的布局和效果的动态改变,我们将这些改变抽象成状态,存放在VM当中。至此,一个最最简单的MVVM元组得以实现。

     
    image.png

    第四篇:MVVM基于APP的应用

    回到市面上较为流行的一种类“MVVM实践模式”,它们以“MVC+VC减负+概念抽象封装”作为基本的思路参考,让VC作为VM和View沟通的主桥梁

    如下图,一般是一个VC包含一个VM和一个主View,然后VM或许会处理少量“双向绑定的任务”,同时也可能将更多的比如网络答复的操作动作回调给VC去处理视图更新

     
    image.png

    这种模式易于理解也确实可以实际的提高代码的概念性和可维护性。但我们发现,视图的更新走了两条长线
    1) 介由VM的绑定实现模型更新视图;
    2) 借由VM的回调实现VC控制更新视图。

    这总让我们感到不够清爽:当我希望将视图中的一段文字由“我的领导是个坏人”改为“我的领导是个好人”时候,没有明确的概念告诉我哪一条“线”是有决策力的“线”(可以成功进行修改的线)。

    “看代码不就知道了?”
    请记住:
    1 有思想的代码几乎不需要透过代码来定位问题
    2 维护代价的“积累”不是“叠加”而是“逻辑分支的叠乘”(每一次“选线”的犹豫,都是一层逻辑分支)

    所以,看代码当然可以解决问题!甚至针对复杂的工程,你大可花费一个月将它的每个细节流程完全理透!然后心满意足的大赞自我的耐心和代码阅读能力!不想,领导已经看到了那句你还没有来得及改掉的“我的领导是个坏蛋”……

    言归正传,第三篇我们基于MVVM在WPF中的应用分析貌似还蛮顺畅的,但好像应用再APP中,有什么地方有些……怪!根源在哪里?或许如下几个问题可以作为我们的参考:
    1) VC的地位到底更倾向于什么?是V?是C?是VM?
    2) 网络请求/视图生命周期/路由跳转这些在APP端大量出现的概念模块,它们在MVVM中有着怎样的概念归属?

    4.1 MVVM基于APP的基础架构&模块分工

    我们尝试一下下面的这套交互结构

     
    image.png

    首先我们明确一个点,在一个MVC结构中,即便抛开视图后,模型和控制器处理的大部分业务逻辑,都是为视图服务的。我们常说的“重VC”,很大一部分重在视图相关的逻辑或是为之服务的逻辑

    所以,当我们抽象一些视图的基础模型,并通过VM将视图本身的(不需要与外界交互的)状态变迁逻辑封装在一个MVVM组的内部,对外(对VC)只暴露必要的数据更新和消息回调接口。繁琐的视图逻辑就可以被限制在一个MVVM当中(它确实也应当在那里)。这时留在VC中的逻辑,一般情况下就很少了。如果此刻的VC还让你感到“重”的话,我们大可再对其抽象一个VC-Logic,将复杂的逻辑进行封装。

    各个模块所负责的主要工作可以参考下图

     
    image.png

    如图,VC中的“生命周期控制”,“网络请求”,“路由”,View中的“视图布局”,“控件效果”都很好理解,让人一眼摸不清的概念主要存在所谓的VM当中,我们来简单说明:

    1)什么是“处理视图状态”?
    视图可能根据不同的状态有不同的展示内容,甚至展示效果。我们常见的“cur”(current)前缀就适用于说明这种场景。“当前选择的模块”,“某个按钮当前的选择状态”,这些表示视图状态的操作变量的定义应当在VM当中,相关的逻辑交互也应当在VM当中。如果说View提供了视图的所有展示元素;那么VM则可以确定某个视图模块某一时刻某一个状态下的呈现内容

    2)什么是“处理视图协作”?
    一个VM不一定只和一个View存在关联,它可能同时协调多个视图。
    我们以同程旅行的一个筛选界面作为参考场景进行说明:

     
    image.png

    当我们将“4.5分以上”后面的对号勾上的时候,上面的“4.5分以上”会被同步勾取,同时,“评分”后面会多出个小绿点,这表示评分这页的筛选条件选择的不是默认的“不限”。很显然,关键词模块、筛选分类模块、筛选详情模块正常人都会分成3部分视图绘制。这三个视图间显然是有交互关系的(即“筛选详情模块”的勾选触发了“关键词模块”的高亮和“筛选分类模块”的加点),而VM即是处理这种交互关系理想场所。

    3)什么是“数据绑定”?
    这边特指将一个模型数据和视图中的一个展示内容进行关联绑定;
    单向绑定一般指模型数据变化触发对应的视图数据变化
    双向绑定指模型数据,视图数据任意一方变化,都会触发另一方的同步变化。

    4)什么是“数据转换”?
    我们不能企望所有的模型数据都能直接被视图使用,比如模型中是一个BOOL(0/1)值,而对应的视图展示期望为“是”/“否”,类似这样的数据转化工作,交给VM吧!

    4.2 MVVM基于APP的抽象讨论

    我们再来讨论一下几个观点的理解:
    1)VC是特殊的VM
    很常见的,VC中除了主要的展示视图外,还有一个导航条(NavigationBar),而我们又很常见导航条要根据主视图的滚动而改变展示效果(比如随着视图滚动变得透明),这种视图的交互显然只能在VC中处理。这很正常,VC可以理解为特殊的VM,即它会负责一些类似VM的协调工作(协调本身也是C的职责),亦会负责VC的其他本职工作(如控制视图生命周期等)

    2)VM的是可以存在类似View的层次的
    写视图,Subview(子视图)的概念是逃不掉的,而参照“将VM理解为视图”的思路,复杂视图中,VM的层次也是逃不掉的,像图中一样。

     
    image.png

    大家会发现,我在VC下面标明了“mainVM”,在一些MVVM下面标明了“mini”,mainVM好理解,因为前面我们已经引入了“VC是特殊的VM这一思路”,但是mini呢?

    大多数场景,我们一个页面的视图交互不会特别复杂,所以,一般的多层视图,只用一个VM管理就够了。但有时我们会希望对视图层中的小模块进行MVVM封装,因为它是“通用”的(希望被复用的),通用的小视图模块往往是“简单”的,这是mini的第一层含义。

    同时,当我们没有引入VM概念的时候,View就单纯地是View么?想想UIButton吧,可以设定选择状态不说,它还可以随时获取当前按钮的选择状态(selected),这不就是说UIButton保存了视图状态么!如果把UIButton进行细致的概念拆分,不就变成了我们的MVVM组么!所以,我们很多系统的视图控件,本来就可以理解为mini的MVVM

    VM从某种角度上讲,就是一个视图!

    第五篇:RAC对iOS实践MVVM的价值

    很显然,上面的讲述中,我们只字未提到RAC(ReactiveCocoa),所以,RAC本身是和MVVM没有本质上的关联的。但无可反驳的是,使用RAC确实能让MVVM的实践上显得更加精巧。

    5.1 快速绑定

    我们在应用中运用的视图更新接口,block回调,代理,通知,KVO,目标动作对……都可以理解为广义“绑定”所依赖的技巧RAC则将上述机制统一成“消息”,可以让我们以更简单的方式处理绑定动作。请看下面的例子。

    1)常规方式:双向绑定一个字符串和一个textField的text值

    /* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
    [_textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged];
    
    - (void)valueChanged:(UITextField *)textField {
        _textStr = _textField.text;
    }
    
    /* 2. 使textStr改变时,textField中的text可以同步变化 */
    [self addObserver:self
           forKeyPath:@"textStr"
              options:NSKeyValueObservingOptionNew
              context:nil];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        if (object == self && [keyPath isEqualToString:@"textStr"]) {
            _textField.text = _textStr;
        }
    }
    

    2)RAC方式:双向绑定一个字符串和一个textField的text值

    /* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
    RAC(self, textStr) = _textField.rac_textSignal;
    /* 2. 使textStr改变时,textField中的text可以同步变化 */
    RAC(self.textField, text) = RACObserve(self, textStr);
    

    代码的简化是显而易见的。

    5.2 多元监听

    iOS中对于代理的应用场景还是很多的。而基本的代理模式中,某个模块的代理者只能有一个。为了让多个对象同时接收代理消息,我们不得不修改模块结构,又或者自定制一个自以为很简单完美的代理队列,又或将代理改用通知?!(不想玩死自己的话,放弃在局部使用这种思路吧!)甚至,还有更奇葩的设计
    然而,在RAC中很简单。

    下面的代码即实现了textStr和textStr2同时监听textField的text的变化
    (处理代理一样的简单,因为RAC全部将其抽象成为了“消息”)

        RAC(self, textStr) = _textField.rac_textSignal;
        RAC(self, textStr2) = _textField.rac_textSignal;
    

    但是,应用中的意义呢?
    将我们封装的VM可以理解为一个模块,对一个模块而言,没什么比输入输出接口的设计更加重要了。而互联网时代,神奇的需求变动在很多场景下让我们不得不对模块进行更新,甚至更新模块的对外接口。多元监听的支持可以大大降低模块对外接口更新的复杂性。(接口的更新很容易牵连整个模块的基础框架,更细节的分析在此不再赘述)

    5.3 元组的引入

    元组,并不是一个让人感到陌生的概念,它即代表一组约定的有序数据,该组数据中每个数据的数据结构不需要统一。
    在MVVM中(其实普通的视图设计中也是),为了方便视图的展示,我们常常要约定一些轻量级的纯视图数据结构。这时候Tuple或许会是最契合我们场景的概念。

    为什么?
    1)Tuple的概念定位不同于Array,tuple的长度一般是确定的,tuple组内每个元素的类型不要求一致
    2)Tuple的概念亦不同于Dictionary,tuple无须将一个数组分为key,value两个部分(繁琐,麻烦~),同时,tuple是有序的(字典是无序的)。

    当然,介于tuple的灵活性特征,使用场景一定要控制在小范围,需要定义对象的时候,还是要定义的!万万不可将tuple在不可控的大范围使用。(一样是玩死自己的行为)

    5.4 没有RAC不能应用MVVM?

    我不这么认为:
    1)如我们之前说过的,MVVM与RAC没有本质的关联
    2)如5.1~5.3,RAC可以使我们应用MVVM的一些场景变得更为简单优雅,RAC针对MVVM优化的问题在我们不使用MVVM时依然也存在(你用MVC是有些场景一样要用KVO),而且,这些场景不算是决定性的(双向绑定的实践应用场景其实很少)

    结语:文章摘自网络,作个人学习用。

  • 相关阅读:
    事件
    DOM中对象的获得
    C# 字符串 相关操作
    两个listbox 复制
    C#窗体控件简介ListBox
    store procedure
    view_baseInfo
    不走弯路,就是捷径
    inherit
    Excel 版本对应
  • 原文地址:https://www.cnblogs.com/guozg/p/9318960.html
Copyright © 2020-2023  润新知