MVVM架构
MVVM架构是Model-View-ViewModel的简写,是一种软件设计模式,将应用程序分成3个层次:
- Model (模型):可以在Model中定义数据修改和操作的业务逻辑
- View (视图):代表UI 组件,可以理解为DOM
- ViewModel (视图模型) :是同步View 和 Model的对象
MVVM架构要实现的是数据和视图的分离,并通过视图模型作为桥梁,来同步数据和视图,开发者只需关注业务逻辑,不需要手动进行繁琐的DOM操作。
更简单的说就是通过中间层视图模型来实现数据和视图的自动同步。
Vue.js
Vue是一套构建用户界面的渐进式框架,是目前很火的一个JavaScript MVVM库。
Vue中ViewModel就是一个Vue实例,它是Vue.js的核心。
理解VUE实现MVVM的方式
VUE将数据保存成纯js对象(类似JSON格式),观察到数据的变化,就对视图对应的内容进行更新。
数据来源可以是通过后台api请求到的,websocket等方式接收到的,可以是浏览器存储的(如:localStorage),也可以是前端自己定义的。
VUE通过模板来实现虚拟视图,监听到视图的变化,就通知数据发生变化。
模板看起来很像DOM,是 HTML、表达式、指令组合起来的特殊语法的字符串,模板被Vue转换为一个JS函数(render渲染函数),生成虚拟DOM,最终转换为html渲染页面。
原理
VUE用数据劫持结合发布者-订阅者模式
的方式实现MVVM模式的。通过Object.defineProperty()
来劫持数据对象各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
当执行 new Vue() 时,Vue 就进入了初始化阶段,实例化后有3个部分来实现实现数据与视图的同步:
-
Observer 数据监听器/观察者
劫持并监听数据 :负责遍历data选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能。 -
Compile 解析器
对元素节点进行遍历,对指令和插值等进行解析,初始化视图,并订阅数据更新视图。 -
Watcher 订阅者
Watcher将数据监听器和指令解析器连接起来,数据的属性变动时,执行指令绑定的相应回调函数,更新视图。
数据的订阅者可能不止一个,所以需要有一个消息订阅器Dep来收集订阅者,Dep负责添加订阅者和遍历订阅者列表通知订阅者更新视图。
具体说当数据发生变化时,Observer 中的 set 方法被触发,在set 方法中调用Dep.notify(),Dep开始遍历所有的订阅者,并调用订阅者的 update方法更新视图。
现在开始模拟实现VUE的数据绑定,首先定义一个视图,按照Vue模板的语法写这么一段html代码
<div id="app">
<input type='text' v-model='title'>
<p id="title">{{title}}</p>
<p id="title2">{{title}}</p>
</div>
在这里我们期望的是在js中定义title的值,视图中不需通过dom就能够显直接示这个值,即视图中有两个title的订阅者,input定义了一个属性v-model将用来实现双向数据绑定(在input中输入自动同步title),后面我们将用Compile解析器来解析指令v-model和用{{}}包裹的数据。
1、模拟创建一个Vue构造函数,生成一个实例
<script>
function Vue(options){
this.data = options.data;
this.id = options.el;
//在这里调用observer劫持数据...
//在这里渲染视图中的数据...
};
let myVue=new Vue({
el:'app',
data:{
title:'hello world',
content:`Content of the *******`
}
});
console.log(myVue);
</script>
<div id="app">
<input type='text' v-model='title'>
<p id="title">{{title}}</p>
<p id="title2">{{title}}</p>
</div>
2、劫持并监听数据
function Vue(options){
...
//劫持数据
observer(this.data,this);
...
};
function observer(data, vm){
Object.keys(data).forEach(key=>{
Object.defineProperty(vm,key,{
get(){
//初始化时在这里添加订阅...
return data[key];
},
set(newVal){
if(data[key] === newVal){ return };
data[key] = newVal;
console.log(`数据${key}被修改了`);
//在这里通知变化...
}
})
})
}
在数据劫持后,myVue的变化如下:
在数据劫持后,获取数据和修改数据时将触发get和set方法。
3、解析指令,初始化视图
//修改Vue构造函数
function Vue(options){
...
//B 渲染视图中的数据
let childNodes=document.getElementById(this.id).childNodes;
Object.keys(childNodes).forEach((i)=>compile(childNodes[i], this));
...
};
//解析指令,初始化视图
function compile (node, vm){
if (node.nodeType === 1) {
// 1、遍历并解析节点属性,被解析的属性成为指令
let attr=node.attributes;
Object.keys(attr).forEach(i=>{
//解析v-model指令
if(attr[i].nodeName === 'v-model'){
let attrName=attr[i].nodeValue;
node.value = vm.data[attrName]; // 给节点的value属性赋值为v-model指定的数据的值
node.removeAttribute('v-model'); //移除v-model
// 在这里监听input事件...
}
//解析其他指令...
});
//2、遍历元素中的text节点,替换掉节点中的{{}}
Object.keys(node.childNodes).forEach(i=>{
let childNode=node.childNodes[i];
if(childNode.nodeType === 3) {
if(/\{\{(.*)\}\}/.test(childNode.nodeValue)) {
let dataKey = RegExp.$1;
childNode.nodeValue = vm.data[dataKey]; //给节点赋值,替换掉{{}}
//在这里新建一个订阅者...
}
}
})
}
};
到这里数据就渲染出来了,但是通过input修改数据时不会有响应,下面来实现双向数据绑定
4、结合调度员和订阅者实现双向数据绑定
首先修改函数compile,①监听input事件 ②根据订阅了数据的dom新建订阅者
// 容纳订阅者的消息订阅器 Dep
class Dep {
constructor() {
this.subs = [];
this.target = null;
};
addSub(sub) {
this.subs.push(sub);
console.log('被添加的订阅者是:', sub);
};
notify() {
this.subs.forEach(sub=>sub.update());
console.log('要通知的订阅者:', this.subs);
}
};
// 订阅者
class Watcher {
constructor(vm, node, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
};
update() {
this.value = this.vm[this.name]; //给Watcher实例value属性赋值,同时将触发改数据的get方法
this.node.nodeValue = this.value; //更新视图
};
};
function compile (node, vm){
...
// 监听input事件
node.addEventListener('input', function(e){
vm[attrName] = e.target.value; // 给相应的data属性赋值,触发绑定的set方法
});
...
//新建一个订阅者
new Watcher(vm, childNode, dataKey);
...
}
接着修改observer函数
function observer(data, vm) {
let dep = new Dep();
Object.keys(data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
//初始化时添加订阅
if (Dep.target) {
dep.addSub(Dep.target); //视图初始化时,向消息订阅器中添加一个订阅者(Dep.target)
}
return data[key];
},
set(newVal) {
if (data[key] === newVal) {return}
data[key] = newVal;
//通知订阅者发生变化...
dep.notify();
}
})
})
}