关于 MVVM和MVC的这些,你知道吗?

我的需求

晚上练完车之后,之前参考我毕设的一个小伙伴要答辩,问了我一个问题,结果问的一下不知道怎么回答…以下是我回答他问题的答案:所以在回答完他之后,赶快整理一波…
在这里插入图片描述

我需要解决的问题

MVVM到底是个什么东东,和前后端有没有关系,它和MVC区别是啥,有啥优势。

我是这样做的

  • 百度寻找,找了一些关于MVVM论文,博客,梳理出自己的答案。
  • 嗯,资源比较零散,准确性有待考量,所以不对的地方请小伙伴指出来

爱自己,是终生浪漫的开始 ——王尔德


对于MVC想来小伙伴是不陌生的,但是网上的资源各抒己见…我也整的晕头转向的,可能有前(后)端,有胖(瘦)客户端框架应用,具体还有细微的差异。

If you put ten software architects into a room and have them discuss what the Model-View-Controller pattern is, you will end up with twelve different opinions. –Josh Smith[^3]
如果你把10个软件架构师放在一个房间里,让他们讨论模型-视图-控制器模式是什么,你最终会得到12种不同的观点。

我们这里讨论的MVCMVVM是以BS架构为基础的java Web中的应用,因为博主只接触了这方面的,关于网上提到的IOS和一些客户端框架,没有接触过。本博客也不涉及。所以如果听都没听过java Web的,或者没了解过 Web框架的小伙伴个人感觉这篇博客不太适合,不太建议继续读下去。

我们先看看MVVM吧!嘻嘻 ^ _ ^

MVVM 名词解释:

MVVMModel-View-ViewModel的简写。它本质上就是MVC的改进版。MVVM 就是将其中的View的状态和行为抽象化,让我们将视图 UI业务逻辑分开。当然这些事ViewModel已经帮我们做了,它可以取出 Model 的数据同时帮忙处理View中由于需要展示内容而涉及的业务逻辑。MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式WPF结合的应用方式时发展演变过来的一种新型架构框架。它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化。^1

MVVMupright=1.5 MVVM(Model–view–viewmodel)是一种软件架构模式MVVM有助于将图形用户界面的开发business logic(业务逻辑)或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或GUI代码实现的。MVVM的视图模型是一个值转换器, 这意味着视图模型负责从模型暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型视图做得更多,并且处理大部分视图显示逻辑。 视图模型可以实现中介者模式,组织对视图所支持的用例集(Model)的后端逻辑的访问。 ^2

MVVM 的发展历程

MVVM马丁·福勒PM(Presentation Model)设计模式的变体。MVVM以相同的方式抽象视图的状态和行为, 但PM不依赖于特定用户界面平台的方式抽象出视图(建立了视图模型)。 MVVMPM都来自MVC模式MVVM由微软架构师Ken CooperTed Peters开发,通过利用WPF(微软.NET图形系统)Silverlight(WPF的互联网应用衍生品)的特性来简化用户界面事件驱动程式设计。 微软的WPF和Silverlight架构师之一John Gossman2005年在他的博客上发表了MVVM。 MVVM也被称为model-view-binder,特别是在不涉及.NET平台的实现中。ZK(Java写的一个Web应用框架)和KnockoutJS(一个JavaScript库)使用model-view-binder^2

MVC到MVVM 的发展历程

二十世纪八十年代施乐帕克实验室提出了MVC的概念,MVC的全称即Model-View-Controller,是模型(model)视图(view)控制器(controller)的缩写“…,它是一种客户端软件开发框架[^4],个人认为,其实最初的Java Web来讲,Model2Servlet+JSP也是用的这个结构,所以说Model2(MVC)它相对已Model1(Javabean+JSP)来讲,已经实现了ViewModel的部分解耦,但是不彻底,如图

Java Web Model2
view负责显示,Model负责提供数据,Controller负责逻辑的处理,其实现的流程大概是:[^4]

  • (1)当用户需要发送请求时,首先是在View发送请求,由View将指令传送到Controller里。
  • (2)Controller接收到指令之后,先完成所需要的业务逻辑,然后要求Model根据业务逻辑改变状态;
  • (3)Model将新的数据发送给View,View则根据新的数据更新视图,从而用户的请求得到反馈。

MVC框架中,View是可以直接访问Model的(JSP里直接使用JavaBean),这样不可避免的使View里面也需要包括一些业务逻辑,同时还需要Model保持不变,而Model又对应着多个不同的显示(View),所以总体说来就是,在MVC模型里面,Model不依赖View,但是View是依赖于Model的。这样就导致更改View比较困难,且业务无法重用。从而MVC框架的弊端就显现出来[^4],这也是使用Servlet+JSP的弊端。前后端没有解耦,ModelView没有彻底解耦。

为了解决MVC框架中ViewModel联系紧密的问题,开发者研究开发了MVP模式,MVPModel-View-Presenter,即把MVC中的Controller换成了Presenter,目的就是为了完全切断ViewModel之间的联系,在MVP模式中,View负责视图的显示,Model负责提供数据Presenter则主要负责逻辑业务的处理。[^4]

有些SSM+JSP的开发方式也是基于这种,我之前的公司就这样写,前后端不分离使用的JSP,但是交互全是Ajax,传递的全是JSON,也没有返回ModelAndView,个人感觉这里其实是使用了MVP的模式。以前后端不分离的方式丢弃模板引擎的服务端渲染,追求前后端分离彻底解耦了View和Model。看上去怪怪的,其实有时候项目开发更多的是和业务体量成本效益等有关系,综合考虑,选最合适,不一定要按照常规构建方式考虑,比如正常思考可能不分离是为了服务端渲染首屏快载SEO等,分离是为了降低服务器压力接口复用,前后端工作职责解耦.

对于SSM+模板引擎的开发方式

  • 如何是返回Modelandview的话,那缺点就是后端路由,前后端没有彻底解耦,优点就是服务端渲染,返回的是整个构建好的页面.
  • 如果返回JSON的话,那优点就是前后端彻底解耦接口复用,但是没有利用模板引擎的服务端渲染
  • 如果体量很大,那前后端是两个人写,那使用Modelandview的方式就很麻烦,需要接口协调,而且工作职责不清晰。会浪费好多时间。JSON就方便很多。
  • 如果体量不是他大,前端的东西也不是特别多,考虑成本问题,前后端一个人写,那Modelandview就很合适,节省了接口协调,对接等时间成本问题。

在这里插入图片描述
MVP框架中,View无法直接再与Model交互,ViewModel之间的通信都是通过Presenter进行完成的,所有的交互都在Presenter内部发生,即由Presenter充当了ViewModel的桥梁,做到View-Model之间通信的完全隔离Presenter完全把ModelView进行分离,将主要的程序逻辑放在Presenter里实现。[^4]

PresenterView也是没有直接相关联的,而是通过已定义的接口进行交互,从而使得在变更View的时候可以保持Presenter的不变,即保证了Presenter的可重用性(接口的复用性),同时也解决了MVC框架中的ViewModel关联紧密的问题。[^4]

这样之后,对于Web项目来讲,前后端都是通过数据进行交互,那路由怎么处理,前端只能实现简单一部分跳转,涉及到复杂的需要通过Controller(Presenter)来处理的路由怎么处理,或者带状态的路由如何跳转,即Controller无法控制使用那个View。个人感觉,Web系统来讲这个时候完全的前后端分离可能不是适合所有项目,而且分离之后留给前端要解决的问题可能也不是能很好的解决。所以这个时候…

有个叫Rod Johnson带领一帮人搞出的SpringMVC,不像桌面应用的MVC, 这里的Model没法给View 发通知。[^5]也不像MVP, 这里的Controller可以控制View来实现路由。即前后后端没有分离,但是将原来的View的构建解耦了。由模板数据构成:

public class MyGlobalException {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ModelAndView customException(MaxUploadSizeExceededException e) {
        ModelAndView mv = new ModelAndView("javaboy");
        mv.addObject("error", e.getMessage());
        return mv;
    }
}

即降低了ViewModel耦合,同时又实现了后端路由
在这里插入图片描述
对于大型项目而言,前端的东西原来越多,造成服务端的压力越来越大,而且由于MVP的出现,逐渐向前后端分离靠拢,分离之后,View分担服务端的压力,或者说是浏览器分担了服务器压力,包括页面渲染,路由等问题,这时侯MVVM出现了…(这里是自己猜的,没找到相关资料

MVVM框架便是前后端分离框架发展史上的一次思想的完全变革。它是将数据模型双向绑定的思想作为变革的核心,即View的变动,自动反映在ViewModel上面,而ViewModel的变动也会随即反映在View上面,从而实现数据与模型的双向绑定。[^4]

MVVM框架中,View用于发送用户的交互请求,之后将用户请求转交给ViewModelViewModel即可根据用户请求操作Model数据更新,待Model数据更新完毕,便会通知ViewModel数据发生了变化,然后ViewModel就会即刻更新View数据,完成视图的更新,从而完成用户的请求。[^4]
在这里插入图片描述

虽然MVVM框架和之前的MVCMVP模式的目的相同,即完成视图(View)和模型(Model)的分离,但它却有着明显的优势。[^4]

  • 首先,MVVM框架中的View完全可以独立于Model发生变化和修改,彻底解耦,View发生变化时Model可以不变,同样,当Model发生变化时View也可以不变化,并且一个ViewModel可以绑定到多个不同的View上面,这就体现了MVVM框架的低耦合性
  • 其次,绑定在一个ViewModel上面的多个View都可以使用ViewModel里面的视图逻辑,完成了框架可重用性的特性。除此之外,MVVM框架还具有可独立开发可测试等特性,把框架作用发挥到最大化,也因此成为了开发者们青睐的框架。。

对于MVVM这种模式主要用于构建基于事件驱动的 UI 平台,对于前端开发领域中数据与界面相混合的情况特别适用[^6],其中

  • Model 仅仅只是代表应用程序所需的数据信息,它不关注任何行为;
  • View 是软件中与用户进行直接交互的部分,它需要响应 ViewModel 的事件并格式化数据,不负责控制应用的状态;
  • ViewModel 用于封装业务逻辑层,这点类似于 MVC 模式中的控制器,它控制View的很多显示逻辑,它可以把数据模型的变化传递视图,也可以把视图中数据的变化传递给数据模型,即在 Model 和View 之间建立了双向绑定。
Vue与MVVM

我第一次看到MVVM是因为Vue,相信好多小伙伴也是Vue认识MVVM架构模式。Vue官网中讲到:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示组件实例
在这里插入图片描述
通过双向数据绑定连接视图层和数据,而实际的界面 UI 操作DOM 操作)被封装成对应的指令(Directives)和过滤器(Filters)

MVVM原理:[^7]

实现数据绑定的做法有大致如下几种:

  • 脏值检查(angular.js): angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测.
    • DOM事件,譬如用户输入文本,点击按钮等。( ng-click)
    • XHR响应事件 ($http )
    • 浏览器Location变更事件 ( $location )
    • Timer事件( $timeout , $interval )
    • 执行 $digest()$apply()
  • 数据劫持(vue.js):数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。简单地说,就是当我们触发函数的时候 动一些手脚做点我们自己想做的事情,也就是所谓的 "劫持"操作
    • 在Vue中其实就是通过Object.defineProperty来劫持对象属性的setter和getter操作,并“种下”一个监听器,当数据发生变化的时候发出通知:Object.defineProperty(obj,prop,descriptor)
      参数:
      obj:目标对象
      prop:需要定义的属性或方法的名称
      descriptor:目标属性所拥有的特性
      可供定义的特性列表:
      value:属性的值
      writable:如果为false,属性的值就不能被重写。
      get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
      set:一旦目标属性被赋值,就会调回此方法。
      configurable:如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
      enumerable:是否能在for…in循环中遍历出来或在Object.keys中列举出来。

    • Proxy数据代理:Proxy 可以被认为是Object.defineProperty() 的升级版。外界对某个对象的访问,都必须经过这层拦截。因此它是针对 整个对象,而不是 对象的某个属性

var data = {name:'test'}
Object.keys(data).forEach(function(key){
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:true,
        get:function(){
            console.log('get');
        },
        set:function(newValue){
            console.log('监听到数据发生了变化');
            document.getElementById(‘myText’).value=newValue;
        }
    })
});
document.getElementById(‘myText’).addEventListener(‘keyup’,function(e){
 data.name=e.target.value; // 监听 View 的变化,同步更新 Model
});
data.name //控制台会打印出 “get”
data.name = 'hxx' //控制台会打印出 "监听到数据发生了变化"
var arr = [1,2,3]
var handle = {
    //target目标对象 key属性名 receiver实际接受的对象
    get(target,key,receiver) {
        console.log(`get ${key}`)
        // Reflect相当于映射到目标对象上
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver) {
        console.log(`set ${key}`)
        return Reflect.set(target,key,value,receiver)
    }
}
//arr要拦截的对象,handle定义拦截行为
var proxy = new Proxy(arr,handle)
proxy.push(4) //可以翻到控制台测试一下会打印出什么

  • 发布者-订阅者模式(backbone.js):

上述介绍了简单的一对一双向绑定的实现,即一个数据模型只与一个视图进行绑定。当多个View与一个 Model进行绑定时,每次更新 Model时需要在Modelset访问器属性中更新多个 View,这样硬编码的方式不利于后期的维护。为了解决硬编码带来的耦合性过强的问题,在在实际实现中,需要使用到设计模式中的发布 - 订阅模式

发布 - 订阅模式(又称观察者模式)是一种常用的设计模式,该模式包含发布者订阅者两种角色。可以让多个订阅者订阅同一个发布者发布的主题,当发布者的主题发生变化时,对外发送一个通知,所有订阅了该主题的订阅者都会接收到更新的消息。因此,观察者模式定义的是一种一对多的关系。发布 – 订阅模式非常适合于 MVVM 双向绑定中多个视图绑定到同一个数据模型的情形。

实现双向数据绑定步骤[^7]

要实现mvvm的双向绑定,就必须要实现以下几点:

  1. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  2. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者(Dep)
  3. 实现一个Watcher,Watcher是订阅 – 发布模式中订阅者的实现,作为连接ObserverCompile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回函数 (发布),从而更新视图
  4. MVVM入口函数,整合以上三者
    在这里插入图片描述
    新建一个Vue 对象时,框架进入初始化阶段。Vue 在初始化阶段主要执行两个操作:
  • 第一个是遍历系统中数据的所有属性,来对各个属性的变化添加监听
  • 第二个操作是利用指令编译器 Compile对视图中绑定的指令进行扫描进行视图的初始化,然后订阅 Watcher更新视图,此时 Watcher 会将自己添加到消息订阅器Dep中。至此,Vue的初始化过程结束。

在系统运行过程中,一旦系统中的数据模型发生了变化,观察者 Observer的 setter 访问器属性就会被触发,此时消息订阅中心 Dep 会遍历它所维护的所有订阅者,对于每一个订阅了该数据的对象,向它发出一个更新通知,订阅者收到通知后就会对视图进行相应的更新。以上过程不断往复循环,这就是 MVVM 模式在 Vue.js 中的运行原理。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Two-way data-binding</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    <script>
        function observe (obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            });
        }
        function defineReactive (obj, key, val) {
            var dep = new Dep();
            Object.defineProperty(obj, key, {
                get: function () {
                    if (Dep.target) dep.addSub(Dep.target);
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal;
                    dep.notify();
                }
            });
        }
        function nodeToFragment (node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child);
            }
            return flag;
        }
        function compile (node, vm) {
            var reg = /\{\{(.*)\}\}/;
            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发该属性的set方法
                            vm[name] = e.target.value;
                        });
                        node.value = vm[name]; // 将data的值赋给该node
                        node.removeAttribute('v-model');
                    }
                }
                new Watcher(vm, node, name, 'input');
            }
            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    new Watcher(vm, node, name, 'text');
                }
            }
        }
    
        function Watcher (vm, node, name, nodeType) {
        //  this为watcher函数
            Dep.target = this;
        //  console.log(this);
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            this.update();
            Dep.target = null;
        }
        Watcher.prototype = {
            update: function () {
                this.get();
                if (this.nodeType == 'text') {
                    this.node.nodeValue = this.value;
                }
                if (this.nodeType == 'input') {
                    this.node.value = this.value;
                }
            },
            // 获取daa中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }
        function Dep () {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function(sub) {
                this.subs.push(sub);
            },
            notify: function() {
                this.subs.forEach(function(sub) {
                    sub.update();
                });
            }
        };
        function Vue (options) {
            this.data = options.data;
            var data = this.data;
            observe(data, this);
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中
            document.getElementById(id).appendChild(dom);
        }
        var vm = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });
    </script>
</body>
</html>


我的理解

  • 架构意义角度(Web端的角度)MVCMVVM在本质上都是为了实现View和Model的解耦MVC是通过Controller实现了ViewModel解耦,一般用与客户端,或者Web端的整个架构过程;而MVVM是在MVC发展到MVP后(为了彻底解决View和Model的耦合问题),在提出前后端分离的基础上(考虑Coltroller的复用性,接口复用性),对View层进行了增强(Vue.js),或者说细化了View层的表现手法,提出了通过ViewModel对视图层的ViewModel解耦。
    个人感觉MVVMMVP的整体架构是有相似的地方的,不同的是面对的问题域不同,MVPWeb架构整体的解决方案,MVVM主要用于构建基于事件驱动的 UI 平台(界面),适用于前端开发领域中数据与界面相混合的情况,所以它只专注于视图层抽象视图的状态和行为,实现了用户界面的UI(View)数据(Model)解耦。这个ViewModel虽然和MVC中描述的一样,但是不相同的,可以理解为MVCView中包含了MVVM的架构方式。
    一般前后端分离Web开发中会结合MVCMVVM两种架构模式。使用MVC构建整体的Web架构,使用MVVM解决ViewDOMdata的耦合问题。
    在这里插入图片描述

  • 设计模式角度考虑MVC是基于观察者设计模式的,Model作为一个主题,View作为观察者,当一个Model变化时,会通知更新一个或多个依赖的View,反之;
    MVVM可以看做是基于中介者设计模式和观察者设计模式,ViewModel通过ViewModel这个中介者对象进行交互,解耦了ViewModel的同时实现数据双向绑定
    同时ViewModel 作为一个主题对象ViewModel为两个观察者(或者可以理解为View为主题时,Model为观察者,反之。这里的Model View起到一个注册通知的作用,对于观察者模式的定义,ModelView是主题的行为,但实际变化的是View或者Model个人觉得两种理解都没问题,理解不对的请小伙伴指出来),当Model变化时,ViewModel数据绑定通知并更新与之相关的多个View,反之,当View变化时,ViewModelDOM监听通知更新相关的多个Model

引用文献资料

[^3]:浅析 web 前端 MVVM[db/ol].https://zhuanlan.zhihu.com/p/54355504
[^4]:程桂花.MVVM前后端数据交互中安全机制的研究与实现[D].浙江理工大学硕士学位设计,2017:6-7
[^5]:你真的理解了MVC, MVP, MVVM吗?[db/ol].https://blog.csdn.net/wdr2003/article/details/79811767
[^6]:易剑波.基于 MVVM 模式的 WEB 前端框架的研究[D].计算机工程应用技术,2016.19:76]
[^7]:Vue MVVM理解及原理实现[db/ol].https://juejin.cn/post/6844903929298288647

免责声明:务必仔细阅读

  • 本站为个人博客,博客所转载的一切破解、path、补丁、注册机和注册信息及软件等资源文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。

  • 本站为非盈利性站点,打赏作为用户喜欢本站捐赠打赏功能,本站不贩卖软件等资源,所有内容不作为商业行为。

  • 本博客的文章中涉及的任何解锁和解密分析脚本,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断.

  • 本博客的任何内容,未经许可禁止任何公众号、自媒体进行任何形式的转载、发布。

  • 博客对任何脚本资源教程问题概不负责,包括但不限于由任何脚本资源教程错误导致的任何损失或损害.

  • 间接使用相关资源或者参照文章的任何用户,包括但不限于建立VPS或在某些行为违反国家/地区法律或相关法规的情况下进行传播, 博客对于由此引起的任何隐私泄漏或其他后果概不负责.

  • 请勿将博客的任何内容用于商业或非法目的,否则后果自负.

  • 如果任何单位或个人认为该博客的任何内容可能涉嫌侵犯其权利,则应及时通知并提供身份证明,所有权证明至admin@proyy.com.我们将在收到认证文件后删除相关内容.

  • 任何以任何方式查看此博客的任何内容的人或直接或间接使用该博客的任何内容的使用者都应仔细阅读此声明。博客保留随时更改或补充此免责声明的权利。一旦使用并复制了博客的任何内容,则视为您已接受此免责声明.

您必须在下载后的24小时内从计算机或手机中完全删除以上内容.

您使用或者复制了本博客的任何内容,则视为已接受此声明,请仔细阅读


更多福利请关注一一网络微信公众号或者小程序

一一网络微信公众号
打个小广告,宝塔服务器面板,我用的也是,很方便,重点是免费的也能用,没钱太难了,穷鬼一个,一键全能部署及管理,送你3188元礼包,点我领取https://www.bt.cn/?invite_code=MV9kY3ZwbXo=


一一网络 » 关于 MVVM和MVC的这些,你知道吗?

发表评论

发表评论

一一网络-提供最优质的文章集合

立即查看 了解详情