面试中被问到MVVM,结果只是知道有这个东西,具体原理不清楚,下来在网上找了各位大神的讲解,总结如下,希望能对大家有帮助。
一、MVC
MVC框架是业界广泛接受的一种前端应用框架类型,这种框架把应用分为三个部分:
- Model(模型)负责管理数据,大部分业务逻辑也应该放在Model中;
- View(视图)负责渲染用户界面,应该避免在View中涉及业务逻辑;
- Controller(控制器)负责接受用户输入,根据用户输入调用对应的Model部分逻辑,把产生的数据结果交给View部分,让View渲染出必要的输出。
通信方式如下:
- view传送指令到controller
- controller完成业务逻辑后,model改变状态
- model将新的数据发送到view,用户得到反馈
MVC框架的几个组成部分和请求的关系可以用下图表示:
但是MVC若使用不当,很可能让大量代码都集中在Controller之中,MVC就变成了Massive View Controller。
MVC设计模式的原则:Don't repeat yourself。要求能够复用的代码要尽量复用,来保证重用。
在 MVC 这种设计模式中,我们发现 View 和 Model 都是符合这种原则的。
对于 View 来说,你如果抽象得好,那么一个 App 的动画效果可以很方便地移植到别的 App 上,而 Github 上也有很多 UI 控件,这些控件都是在 View 层做了很好的封装设计,使得它能够方便地开源给大家复用。比如上一个项目中我们用到的前端UI控件ant design,这是一个很好的控件库,所有的UI都已经封装好,随时可以拿来用,不用自己再编写底层的样式实现。
对于 Model 来说,它其实是用来存储业务的数据的,如果做得好,它也可以方便地复用。当然,因为和业务本身的数据意义相关,Model 层的复用大多数是在一个产品内部,不太可能像 View 层那样开源给社区。
如果我们能够意识到 Controller 里面的代码不便于复用,我们就能知道什么代码应该写在 Controller 里面了,那就是那些不能复用的代码。在我看来,Controller 里面就只应该存放这些不能复用的代码,这些代码包括:
- 在初始化时,构造相应的 View 和 Model。
- 监听 Model 层的事件,将 Model 层的数据传递到 View 层。
- 监听 View 层的事件,并且将 View 层的事件转发到 Model 层。
如果 Controller 只有以上的这些代码,那么它的逻辑将非常简单,而且也会非常短。
如何对ViewController瘦身呢,就引入了MVVM。
二、MVVM
MVVM 在使用当中,通常还会利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。
mvvm通信:唯一的区别是MVVM采用了双向数据绑定,view的变动自动反映在view-model。
在 MVVM 框架中,View(视图) 和 Model(数据) 是不可以直接通讯的,在它们之间存在着 ViewModel 这个中间介充当着观察者的角色。当用户操作 View(视图),ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model(数据) 发生改变,ViewModel 也能感知到变化,使 View 作出相应更新。这个一来一回的过程就是我们所熟知的双向绑定。
对此,MVVM 的作者 John Gossman 的批评应该是最为中肯的。John Gossman 对 MVVM 的批评主要有两点:
- 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
- 对于过大的项目,数据绑定需要花费更多的内存。
三、VC 到 VM 的转变
一个 VC 在包含了大量的业务逻辑后,代码就会变得特别的臃肿、不易阅读和修改。于是后来就慢慢延伸出了MVVM 模式。MVVM 模式顾名思义是由 Model、View 和 ViewModel(下称 VM )组成,之前的 VC 变成了 VM。怎么来理解 VM 呢?
对于一个 Model ,比如我们要存储和显示一个人的信息,这个人具有姓名、年龄、性别这三个属性。这个Model的伪代码如下:
class Person {
String name;
int age;
int gender;
}
Model 的数据模型,和我们的业务需求或者说业务实体(Entity)是一一映射关系。而 ViewModel 顾名思义,就是一个 Model of View,它是一个 View 信息的存储结构,ViewModel 和 View 上的信息是一一映射关系。
以一个软件的登陆场景为例子,假设这个登录界面上有如下逻辑:
- 用户名输入框
- 密码输入框
- 登陆按钮,点击登陆按钮按钮置灰,显示 loading 框
- 登陆成功,页面触发跳转
- 登陆失败,loading 框消失,在界面上显示错误信息
- 错误信息可以分为两种情况: 1、密码错误;2、没有网络
那么我们下面来定义这样一个 ViewModel:
class LoginViewModel {
String userId;
String password;
bool isLoading;
bool isShowErrorMessage;
String errorMessage;
}
界面初始化
由于 LoginView 和 LoginViewModel 是映射关系,也称为绑定关系,那么 LoginViewModel 是怎样的数据,View 就按照怎样的数据来进行显示。界面第一次打开时,整个 LoginViewModel 的初始值为:
{
userId: '',
password: '',
isLoading: false,
isShowErrorMessage: false, errorMessage: ''
}
那么此时界面为:
- 用户名输入框显示为空白字符串
- 密码输入框显示为空白字符串
- loading 框因为 isLoading = false,所以不显示
- 错误信息框 因为 isShowErrorMessage = false,所以不显示
- 错误信息框里面的文字为空白字符串
触发登陆
接下来,用户输入用户名和密码,点击登录按钮,在登陆事件里面触发网络通信逻辑,同时设定 isLoading = true,伪代码如下:
function onLoginButtonClick() {
request(url, ...);
loginViewModel.isLoading = true;
}
此时 LoginViewModel 的值为:
{
userId: 'this is user id',
password: 'this is password',
isLoading: true,
isShowErrorMessage: false,
errorMessage: ''
}
随着 isLoading 值的变化,因为 ViewModel 和 View 存在绑定关系,那么此时界面动态变化为:
- 用户名输入框显示为刚刚输入的字符串
- 密码输入框显示为刚刚输入的字符串
- 因为isLoading = true,所以显示 loading 框
- 因为isLoading = true,登陆按钮置灰,不可点击
- 错误信息框 因为 isShowErrorMessage = false,所以不显示
- 错误信息框里面的文字为空白字符串
登录失败
接下来我们假设登陆失败,服务器返回密码错误,那么此时在服务器逻辑的相应代码里面我们去设定 isLoading = false,isShowErrorMessage = true,以及对应的errorMessage,伪代码如下:
request(url, {
success: function() {
...
},
fail: function(err) {
if(err.code == 1000) { // 假设1000表示密码错误
loginViewModel.isLoading = false;
loginViewModel.isShowErrorMessage = true;
loginViewModel.errorMessage = '密码错误';
}
}
})
此时 LoginViewModel 的值为:
{
userId: 'this is user id',
password: 'this is password',
isLoading: false,
isShowErrorMessage: ture,
errorMessage: '密码错误'
}
接下来依然是触发界面变化,根据绑定关系重新更新显示内容:
- 用户名输入框显示为刚刚输入的字符串
- 密码输入框显示为刚刚输入的字符串
- 因为isLoading = false,隐藏 loading 框
- 因为isLoading = false,登陆按钮置为正常,可以点击
- 因为 isShowErrorMessage = true,显示错误信息框
- 错误信息框里面的文字为“密码错误”
个人理解,在广义地谈论MVC架构时,并非指本文中严格定义的MVC,而是指的MV*,也就是视图和模型的分离,只要一个框架提供了视图和模型分离的功能,我们就可以认为它是一个MVC框架。在开发深入之后,可以再体会用到的框架到底是MVC还是MVVM。
参考链接: