我的需求:
晚上练完车之后,之前参考我毕设的一个小伙伴要答辩,问了我一个问题,结果问的一下不知道怎么回答…以下是我回答他问题的答案
:所以在回答完他之后,赶快整理一波…
我需要解决的问题:
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种不同的观点。
我们这里讨论的MVC
和MVVM
是以BS架构
为基础的java Web
中的应用,因为博主只接触了这方面的,关于网上提到的IOS
和一些客户端
框架,没有接触过。本博客也不涉及。所以如果听都没听过java Web
的,或者没了解过 Web框架
的小伙伴个人感觉这篇博客不太适合,不太建议继续读下去。
我们先看看MVVM吧!嘻嘻 ^ _ ^
MVVM 名词解释:
MVVM
是Model-View-ViewModel
的简写。它本质上就是MVC
的改进版。MVVM 就是将其中的View的状态和行为抽象化
,让我们将视图 UI
和业务逻辑
分开。当然这些事ViewModel
已经帮我们做了,它可以取出 Model
的数据同时帮忙处理View
中由于需要展示内容而涉及的业务逻辑。MVVM(Model-View-ViewModel)框架
的由来便是MVP(Model-View-Presenter)模式
与WPF
结合的应用方式时发展演变过来的一种新型架构框架
。它立足于原有MVP
框架并且把WPF
的新特性糅合进去,以应对客户日益复杂的需求变化。^1
MVVM
upright=1.5 MVVM(Model–view–viewmodel)
是一种软件架构模式
。 MVVM
有助于将图形用户界面的开发
与business logic
(业务逻辑)或后端逻辑
(数据模型)的开发分离
开来,这是通过置标语言或GUI代码实现的。MVVM的视图模型是一个值转换器
, 这意味着视图模型
负责从模型
中暴露(转换)数据对象
,以便轻松管理和呈现对象
。在这方面,视图模型
比视图
做得更多,并且处理大部分视图
的显示逻辑
。 视图模型可以实现中介者模式
,组织对视图所支持的用例集(Model)的后端逻辑的访问。 ^2
MVVM 的发展历程
MVVM
是马丁·福勒
的PM(Presentation Model)设计模式
的变体。MVVM
以相同的方式抽象
出视图的状态和行为
, 但PM
以不依赖于特定用户界面平台
的方式抽象出视图
(建立了视图模型)。 MVVM
和PM
都来自MVC模式
。 MVVM
由微软架构师Ken Cooper
和Ted Peters
开发,通过利用WPF(微软.NET图形系统)
和Silverlight(WPF的互联网应用衍生品)
的特性来简化用户界面
的事件驱动程式设计
。 微软的WPF和Silverlight架构师之一John Gossman
于2005
年在他的博客上发表了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
来讲,Model2
即Servlet JSP
也是用的这个结构,所以说Model2(MVC)
它相对已Model1(Javabean JSP)
来讲,已经实现了View
和Model
的部分解耦,但是不彻底,如图
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
的弊端。前后端没有解耦,Model
与View
没有彻底解耦。
为了解决MVC
框架中View
和Model
联系紧密的问题,开发者研究开发了MVP
模式,MVP
即Model-View-Presenter
,即把MVC中的Controller
换成了Presenter
,目的就是为了完全切断View
跟Model
之间的联系,在MVP模式中,View
负责视图的显示
,Model
负责提供数据
,Presenter
则主要负责逻辑业务
的处理。[^4]
有些SSM JSP
的开发方式也是基于这种,我之前的公司就这样写,前后端不分离使用的JSP
,但是交互全是Ajax
,传递的全是JSON
,也没有返回ModelAndView
,个人感觉这里其实是使用了MVP
的模式。以前后端不分离
的方式丢弃模板引擎的服务端渲染
,追求前后端分离
中彻底解耦了View和Model
。看上去怪怪的,其实有时候项目开发更多的是和业务
、体量
、成本
、效益
等有关系,综合考虑,选最合适,不一定要按照常规
的构建方式
考虑,比如正常思考可能不分离
是为了服务端渲染
,首屏快载
,SEO
等,分离是为了降低服务器压力
,接口复用
,前后端工作职责解耦
.
对于SSM
模板引擎
的开发方式
- 如何是返回
Modelandview
的话,那缺点
就是后端路由
,前后端没有彻底解耦
,优点就是服务端渲染
,返回的是整个构建好的页面
. - 如果返回
JSON
的话,那优点就是前后端彻底解耦
,接口复用
,但是没有利用模板引擎的服务端渲染
。 - 如果体量很大,那前后端是两个人写,那使用
Modelandview
的方式就很麻烦,需要接口协调
,而且工作职责不清晰
。会浪费好多时间。JSON就方便很多。 - 如果体量不是他大,前端的东西也不是特别多,考虑成本问题,前后端一个人写,那
Modelandview
就很合适,节省了接口协调,对接等时间成本问题。
在MVP
框架中,View
无法直接再与Model
交互,View
和Model
之间的通信都是通过Presenter
进行完成的,所有的交互都在Presenter
内部发生,即由Presenter
充当了View
和Model
的桥梁,做到View-Model
之间通信的完全隔离
。Presenter
完全把Model
和View
进行分离,将主要的程序逻辑放在Presenter里实现。[^4]
Presenter
与View
也是没有直接相关联的,而是通过已定义的接口进行交互
,从而使得在变更View
的时候可以保持Presenter
的不变,即保证了Presenter
的可重用性(接口的复用性),同时也解决了MVC
框架中的View
和Model
关联紧密的问题。[^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;
}
}
即降低了View
和Model
耦合,同时又实现了后端路由
。
对于大型项目而言,前端
的东西原来越多,造成服务端
的压力越来越大,而且由于MVP
的出现,逐渐向前后端分离
靠拢,分离之后,View分担服务端
的压力,或者说是浏览器
分担了服务器
压力,包括页面渲染,路由等问题,这时侯MVVM出现了…(这里是自己猜的,没找到相关资料)
MVVM
框架便是前后端分离框架发展史
上的一次思想的完全变革。它是将数据模型双向绑定
的思想作为变革的核心
,即View
的变动,自动反映在ViewModel
上面,而ViewModel
的变动也会随即反映在View
上面,从而实现数据与模型的双向绑定
。[^4]
在MVVM
框架中,View
用于发送用户的交互请求
,之后将用户请求转交给ViewModel
,ViewModel
即可根据用户请求操作Model
数据更新,待Model
数据更新完毕,便会通知ViewModel
数据发生了变化,然后ViewModel
就会即刻更新View
数据,完成视图的更新,从而完成用户的请求。[^4]
虽然MVVM
框架和之前的MVC
、MVP
模式的目的相同,即完成视图(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() 的升级版
。外界对某个对象的访问
,都必须经过这层拦截
。因此它是针对 整个对象
,而不是 对象的某个属性
。
- 在Vue中其实就是通过
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' //控制台会打印出 "监听到数据发生了变化"
代码语言:javascript复制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
时需要在Model
的set
访问器属性中更新多个View
,这样硬编码的方式不利于后期的维护
。为了解决硬编码带来的耦合性过强
的问题,在在实际实现中,需要使用到设计模式中的发布 - 订阅模式
。
发布 - 订阅模式
(又称观察者
模式)是一种常用的设计模式,该模式包含发布者
和订阅者
两种角色。可以让多个订阅者
订阅同一个发布者
发布的主题,当发布者的主题发生变化时,对外发送一个通知,所有订阅了该主题的订阅者都会接收到更新的消息。因此,观察者模式定义的是一种一对多的关系
。发布 - 订阅模式非常适合于 MVVM 双向绑定中多个视图绑定到同一个数据模型的情形。
实现双向数据绑定步骤[^7]
要实现mvvm
的双向绑定,就必须要实现以下几点:
- 实现一个
指令解析器Compile
,对每个元素节点的指令进行扫描和解析,根据指令模板
替换数据,以及绑定
相应的更新函数
- 实现一个
数据监听器Observer
,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者(Dep)
- 实现一个
Watcher
,Watcher是订阅 - 发布模式中订阅者的实现,作为连接Observer
和Compile
的桥梁,能够订阅并收到每个属性变动的通知,执行指令
绑定的相应回函数 (发布),从而更新视图
- 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端的角度):
MVC
和MVVM
在本质上都是为了实现View和Model的解耦
,MVC
是通过Controller
实现了View
和Model
的解耦
,一般用与客户端,或者Web
端的整个架构过程;而MVVM
是在MVC
发展到MVP
后(为了彻底解决View和Model的耦合问题),在提出前后端分离
的基础上(考虑Coltroller的复用性,接口复用性),对View层
进行了增强(Vue.js)
,或者说细化了View层的表现手法,提出了通过ViewModel
对视图层的View
和Model
解耦。 个人感觉MVVM
和MVP
的整体架构
是有相似
的地方的,不同的是面对的问题域
不同,MVP
是Web
架构整体的解决方案,MVVM
主要用于构建基于事件驱动的 UI 平台(界面)
,适用于前端
开发领域中数据与界面相混合
的情况,所以它只专注于视图层
,抽象
出视图
的状态和行为,实现了用户界面的UI(View)
和数据(Model)
的解耦
。这个View
和Model
虽然和MVC
中描述的一样,但是不相同的,可以理解为MVC
中View
中包含了MVVM
的架构方式。 一般前后端分离
的Web
开发中会结合MVC
和MVVM
两种架构模式。使用MVC
构建整体的Web
架构,使用MVVM
解决View
层DOM
和data
的耦合问题。
- 设计模式角度考虑 :
MVC
是基于观察者
设计模式的,Model
作为一个主题,View
作为观察者,当一个Model
变化时,会通知更新一个或多个依赖的View
,反之; MVVM可以看做是基于中介者
设计模式和观察者
设计模式,View
和Model
通过ViewModel
这个中介者对象
进行交互,解耦了View
和Mode
l的同时实现数据双向绑定
。 同时ViewModel
作为一个主题对象
,View
和Model
为两个观察者(或者可以理解为View
为主题时,Model
为观察者,反之。这里的Model View
起到一个注册
,通知
的作用,对于观察者
模式的定义,ModelView
是主题的行为,但实际变化的是View
或者Model
,个人觉得两种理解都没问题,理解不对的请小伙伴指出来),当Model
变化时,ViewModel
由数据绑定
通知并更新与之相关的多个View
,反之,当View
变化时,ViewModel
由DOM监听
通知更新相关的多个Model
。