前言
演变历史:
直接操作DOM -> MVC -> MVP -> MVVM
MVC
- M —— model 数据层& 手动渲染模板
- V —— view 视图层
-
C —— controller 控制层(业务逻辑)
流程:
V ——> C ——> M ——> V
- View 传送指令 到 controller
- controller 完成业务逻辑处理后,触发 Model 改变状态
- Model 将新的数据发送到 View, View 更新视图
所有的通信都是单向的
见图
实现代码
// html
<div id="A" onclick="A.event.change"></div>
//js
var A = new app({
//controller
controller() {
let self = this
// 绑定事件
self.event['change'] = function() {
self.model.setValue('text', '新的viewA 的值') // 触发model数据更新
}
},
//model
model(){
this.text = 'viewA 渲染完成';
// 绑定更新回调函数
this.setValue = function(key, val){
this[key] = val // 更新新值
this.view.render(this) // 调用渲染函数
}
}
// view
view(data) {
var tpl = ''<span>{{text}}</span>'
this.render= function(tpl,data){
// 渲染视图
。。。
}
}
})
MVP 是对MVC的一种改造,将手动渲染步骤从Model 移到Presenter层上
- M —— model 数据层
- V —— view 视图层
-
P —— presenter 控制层 & 手动渲染模板
流程:
V <——> P <——> M
- 各部分直接的通信,都是双向的
- View 与 Model 不发生联系,相互对立
见图
实现代码
// html
<div id="A" onclick="A.event.change"></div>
//js
var A = new app({
//presenter
presenter() {
let self = this
// 绑定事件
self.event['change'] = function() {
self.model.text = "新的viewA 的值"
self.view.render(self.model)
}
},
//model
model(){
this.text = 'viewA 渲染完成';
}
// view
view(data) {
var tpl = ''<span>{{text}}</span>'
this.render= function(tpl,data){
// 渲染视图
。。。
}
}
})
MVVM 是将Presenter 改为ViewModel ,基本与MVP模式一致
唯一区别是:它采用双向绑定:View 的变动,自动反映在ViewModel上,反之亦然
- M —— model 数据层
- V —— view 视图层
-
VM —— ViewModel 依靠Directive, 修改数据& 自动渲染模板
流程:
V <——> VM <——> M
图
实现代码
// html
<div id="A" onclick="change">
<span >{{text}}</span>
</div>
//js
new VM({
//model
model:{
text = 'viewA 渲染完成';
},
methods: {
change(){
this.text = "新的viewA 的值"
}
}
})
MVVM模式依靠Directive, 实现了修改数据和模板自动渲染,解放了开发者,只需要关注View和Model,效率和性能提高,低耦合度,对立开发,可复用性
数据变动检查方案 (Directive)
一. 手动触发绑定
在页面需要改变是,手动触发检测,改变Model数据,并扫描元素,对有标记的元素进行修改
let data = {
value: 'hello'
};
let directive = {
html: function (html) {
this.innerHTML = html;
},
value: function (html) {
this.setAttribute('value', value);
}
};
ViewModelSet('value', 'hello world');
function ViewModelSet(key, value) {
data[key] = value;
scan();
}
function scan() {
for (let elem of elems) {
elem.directive = [];
for (let attr of elem.attributes) {
if (attr.nodeName.indexOf('v-') >= 0) {
directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
}
}
}
}
一. 脏检测机制 (Angrlar)
针对手动绑定进行优化,只对修改到的数据进行更新元素
function scan(elems, val) {
let list = document.querySelectorAll(`[v-bind=${val}]`); // 只扫描修改到的数据涉及的元素
for (let elem of elems) {
for (let attr of elem.attributes) {
let dataKey = elem.getAttribute('v-bind');
if (elem.directive[attr.nodeValue] !== data[dataKey]) { // 当元素值有变时,更新元素
directive[attr.nodeValue].call(elem, data[dataKey]);
elem.directive[attr.nodeValue] = data[dataKey]; // 保存元素当前值
}
}
}
}
三. 数据劫持结合订阅发布模式 (Vue)
使用 Object.defineProperty 对数据进行get 和 set 监听,数据变动时,通知订阅者调用更新回调函数,重新渲染视图
详细内容请看我的文章Vue的双向绑定原理及实现
四. ES6 Proxy
与方法三类似,换成ES6的写法
let data = new Proxy({
get: function(obj, key){
return obj[key]
},
set: function(obj,key,val){
obj[key] = val
scan() // 订阅更新回调函数
return obj[key]
}
})
参考文献:
https://segmentfault.com/a/1190000013464776#articleHeader3
http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html