参见知乎文章 浅析前端开发中的 MVC/MVP/MVVM 模式
参见勾三股四大神文章 vue.js源码学习笔记
mv*模式共性 把应用程序的数据、业务逻辑和界面这三块解耦
这些模式 目的是把应用程序的数据、业务逻辑和界面这三块解耦,分离关注点,不仅利于团队协作和测试,更有利于维护和管理(甩锅)。
业务逻辑不再关心底层数据的读写,而这些数据又以对象的形式呈现给业务逻辑层。
从 MVC --> MVP --> MVVM,就像一个打怪升级的过程,它们都是在MVC的基础上随着时代和应用环境的发展衍变而来的。
Model&View 原生实现
没有分层 实现
view层-html
<script src="../jquery-3.6.0.js"></script>
<form action="">
<div>
<input type="text" id="num" />
</div>
<div>
<input type="button" id="increase" value="+" > <input type="button" id="decrease" value="-">
</div>
</form>
逻辑控制层-js
$(function (){
window.num=0;
$("#num").val(num);
$("#increase").click(function (value){
console.log("increase clicked");
num++;
$("#num").val(num);
})
$("#decrease").click(function (value){
console.log("decrease clicked");
num--;
$("#num").val(num);
})
})
从上面代码可以看到,js代码中,包含模型-num, 视图-增加按钮,减少按钮,点击事件绑定(在事件里面直接更改模型num,同时更改视图num的val)
可以看到逻辑控制层是连接 模型和视图的桥梁,关键是,所有的功能都集中在逻辑控制层中,下面就用mvc,mvp,mvvm模式,分别实现这个增减控件功能。
一、mvc模式
MVC允许在不改变视图的情况下,改变视图对用户输入的响应方式,
用户对View的操作交给了Controller处理,
在Controller中响应View的事件调用Model的接口对数据进行操作,
一旦Model发生变化便通知相关视图进行更新。
换句话来说
用户想要改变视图,或者在视图view上交互操作
这个操作交给controller处理,
controller响应view的事情,调用model的接口,对数据操作
一旦,model中的数据发生改变,通过观察者模式,通知视图同时进行更新
Model - Model层用来存储业务的数据,一旦数据发生变化,模型将通知有关的视图。
Model层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。 这里我们把需要用到的数值变量封装在Model中, 并定义了add、sub、getVal三种操作数值方法。
var myapp = {}; // 创建这个应用对象
myapp.Model = function() {
var val = 0; // 需要操作的数据
/* 操作数据的方法 */
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
//* 观察者模式 *
//Model和View之间使用了观察者模式,
// View事先在此Model上注册,进而观察Model,以便更新在Model上发生改变的数据
var self = this,
views = [];
this.register = function(view) {
console.log("register")
views.push(view);
};
this.notify = function() {
console.log("notify")
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};
Model和View之间使用了观察者模式,View事先在此Model上注册,进而观察Model,以便更新在Model上发生改变的数据
View -View作为视图层,主要负责数据的展示。
view和controller之间使用了策略模式, 这里View引入了Controller的实例来实现特定的响应策略
myapp.View = function(controller) {
/* 视图元素 */
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
/* 渲染数据 */
this.render = function(model) {
console.log("render")
$num.val(model.getVal() + 'rmb');
};
//view和controller之间使用了策略模式,
// 这里View引入了Controller的实例来实现特定的响应策略,
// 比如这个栗子中按钮的 `click` 事件:
//如果要实现不同的响应的策略只要用不同的Controller实例替换即可。
/* 绑定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
controller 控制器是模型和视图之间的纽带(响应用户的操作、同步更新View和Model。)
MVC将响应机制封装在controller对象中,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。
myapp.Controller = function() {
var model = null,
view = null;
//这里我们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View做更新,这里用到了观察者模式
this.init = function() {
/* 初始化Model和View */
model = new myapp.Model();
console.log(model)
view = new myapp.View(this);
console.log(view)
/* View向Model注册,当Model更新就会去通知View啦 */
model.register(view);
model.notify();
};
/* 让Model更新数值并通知View更新视图 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};
执行应用的时候,使用Controller做初始化
$(function() {
var controller = new myapp.Controller();
controller.init();
});
整理 mvc页面初始化,以及响应用户操作
1创建一个model,model里面封装有一个val,
当model的数据发生变化,需要通知有关视图
注册视图 regist,通知视图数据发生变化,重新渲染 notice{reder(self)}
2 创建视图,关联视图对应的页面元素,$num $incBtn $decBtn
元素渲染的方法render, render
以及视图里面点击事件触发时,调用的controller中的响应策略 $incBtn.click(controller.increase);
3创建控制器
响应用户的操作,同步更新View和Model increase(model.add(1);model.notify()😉,decrease()
4 执行应用,控制器做初始化
new myapp.Model()
myapp.View(this)
View向Model注册,当Model更新就会去通知View
model.register(view);
model.notify(view.render());
总结
MVC模式的业务逻辑主要集中在Controller,
而前端的View其实已经具备了独立处理用户事件的能力,
当每个事件都流经Controller时,这层会变得十分臃肿。
而且MVC中View和Controller一般是一一对应的,捆绑起来表示一个组件,
视图与控制器间的过于紧密的连接让Controller的复用性成了问题,
如果想多个View共用一个Controller该怎么办呢?
二、MVP模式
Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。
MVP(Model-View-Presenter)是MVC模式的改良,
与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。
Model层 依然是主要与业务相关的数据和对应处理数据的方法
这里可以看到,model与view之间没有mvc模式的regist,和notice(观察者模式),也就是说,模型更改后,不需要在model层,通知视图更新
var myapp = {}; // 创建这个应用对象
//### Model
//Model层依然是主要与业务相关的数据和对应处理数据的方法。
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};
View作为视图层,主要负责数据的展示。
应用程序init已VIew为入口
myapp.View = function() {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
console.log('render')
$num.val(model.getVal() + 'rmb');
};
this.init = function() {
var presenter = new myapp.Presenter(this);
$incBtn.click(presenter.increase);
$decBtn.click(presenter.decrease);
};
};
Presenter(主持人-报幕/通知出场表演者-控制流程)
MVP定义了Presenter和View之间的接口,用户对View的操作都转移到了Presenter。
待Presenter通知Model更新后,Presenter调用View提供的接口(render(model))更新视图。
myapp.Presenter = function(view) {
var _model = new myapp.Model();
var _view = view;
_view.render(_model);
this.increase = function() {
_model.add(1);
_view.render(_model);
};
this.decrease = function() {
_model.sub(1);
_view.render(_model);
};
};
执行程序,初始化
//以View为入口:
$(function (){
console.log('init')
var view = new myapp.View();
view.init();
})
总结
Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”。
三、MVVM模式
MVVM(Model-View-ViewModel)最早由微软提出。ViewModel指 “Model of View”——视图的模型。这个概念曾在一段时间内被前端圈热炒,以至于很多初学者拿jQuery和Vue做对比…
MVVM把View和Model的同步逻辑自动化了
以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。
MVVM比MVC/MVP精简了很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新
与MVP不同的是,没有了View为Presente提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理
当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。
MVVM代码实现
上面说了这么多vue.js以及MVVM模式,下面用vue的方式实现以下MVVM
view {{ val }} v-on:click=“sub(1)”
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="../../js/vue.js"></script>
</head>
<body>
<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>
</body>
</html>
Model
var data = {
val: 0
};
ViewModel
var app=new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v;
}
},
sub(v) {
if(this.val > 0) {
this.val -= v;
}
}
}
});
Vue.js
数据绑定
在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。
不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的前端框架实现数据绑定的方式大致有以下几种:
-
数据劫持 (Vue)
-
发布-订阅模式 (Knockout、Backbone)
-
脏值检查 (Angular)
我们这里主要讲讲Vue。
Vue采用数据劫持&发布-订阅模式的方式,通过ES5提供的 Object.defineProperty()
方法来劫持(监控)各属性的 getter
、setter
,并在数据(对象)发生变动时通知订阅者,触发相应的监听回调。
由于是在不同的数据上触发同步,可以精确的将变更发送给绑定的视图,而不是对所有的数据都执行一次检测。要实现Vue中的双向数据绑定,大致可以划分三个模块:Observer、Compile、Watcher,如图:
- Observer 数据监听器
负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。
- Compiler 指令解析器
扫描模板,并对指令进行解析,然后绑定指定事件。
- Watcher 订阅者
关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。Update()是它自身的一个方法,用于执行Compile中绑定的回调,更新视图。
数据更新的 diff 机制
先比较新旧两个列表的 vm 的数据的状态,然后差量更新 DOM
数据监听机制
我们很容易想到 Object.defineProperty 这个 API,为此属性设计一个特殊的 getter/setter,然后在 setter 里触发一个函数,就可以达到监听的效果。
视图的解析过程
Vue.js 的策略是把 element 或 template string 先统一转换成 document fragment,然后再分解和解析其中的子组件和 directives。我觉得这里有一定的性能优化空间,毕竟 DOM 操作相比之余纯 JavaScript 运算还是会慢一些。
vue.js和诸多 MVVM 的思路是类似的,主要做了三件事:
1.通过 observer 对 data 进行了监听,并且提供订阅某个数据项的变化的能力
2.把 template 解析成一段 document fragment,然后解析其中的 directive,得到每一个 directive 所依赖的数据项及其更新方法。比如 v-text=“message” 被解析之后 (这里仅作示意,实际程序逻辑会更严谨而复杂):
所依赖的数据项 this.
d
a
t
a
.
m
e
s
s
a
g
e
,
以
及
相
应
的
视
图
更
新
方
法
n
o
d
e
.
t
e
x
t
C
o
n
t
e
n
t
=
t
h
i
s
.
data.message,以及 相应的视图更新方法 node.textContent = this.
data.message,以及相应的视图更新方法node.textContent=this.data.message
3.通过 watcher 把上述两部分结合起来,即把 directive 中的数据依赖订阅在对应数据的 observer 上,这样当数据变化的时候,就会触发 observer,进而触发相关依赖对应的视图更新方法,最后达到模板原本的关联效果。
所以整个 vm 的核心,就是如何实现 observer, directive (parser), watcher 这三样东西