(一)MVVM介绍
MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型,MVVM是双向、自动的,也就是数据发生变化自动同步视图,视图发生变化自动同步数据。
-
Model:数据模型),后端传递的数据(data,props,computed等部分)
-
View:代表 UI 组件,它负责将数据模型转化成 UI 展现出来(template),通常是DOM层,主要作用是给用户展示各种信息。
-
ViewModel:是一个同步View 和 Model的对象。MVVM模式的核心,它是连接Model和View的桥梁。
- 实现了Data Binding(数据绑定),将Model的改变实时反应到View中,即将后端传递的数据转化成所看到的页面
- 实现了DOM Listeners(DOM监听),当dom发生事件时,在需要的情况下改变对应的data,即将所看到的页面转化成后端的数据
开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
(二)vue的双向绑定原理
vue数据的双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
(1)发布订阅–观察者模式
- 监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
(2)数据劫持
通过 Object.defineProperty() 方法实现的,vue 在内部会把定义在 data 中的属性通过这个方法全部转为 getter/setter,在数据变化时发布消息给订阅者,触发相应的监听回调,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变
(三)vue的双向绑定实现
-
初始化阶段:
- Observer 劫持 data 对象的属性,给每个属性添加 getter 和 setter 方法。
- Compile 遍历 DOM,解析指令,如 v-model 和 v-bind。
- 为每个解析到的指令创建对应的 Watcher,并添加到对应属性的 Dep 中。
- 将数据渲染到页面。
-
视图到数据:
- 当用户与页面上的表单元素交互时,事件监听器(如 v-model 的输入监听器)触发并修改 data 对象的值。
- 由于表单元素绑定了 data 对象的某个属性,因此修改 data 的值会触发该属性的 setter 方法(Observer 执行 set 和 get)。
-
数据到视图:
- 当后端传来新数据或者数据发生变化时,Observer 会执行 setter 方法,通知 Dep 更新视图。
- Dep 根据通知找到对应的一组 Watcher,并调用它们的 update 方法来更新视图
实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
实现一个订阅者Watcher,每个Watcher都绑定一个更新函数,Watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。由于 data 的某个 key 在一个视图中可能出现多次,所以每个 key 都需要一个管家 Dep 来管理多个Watcher。
实现一个消息订阅器 Dep,主要收集订阅者,当 Observe 监听到发生变化,就通知 Dep 再去通知Watcher去触发更新。
实现一个解析器Compile,可以扫描和解析每个节点的相关指令,若节点存在指令,则Compile初始化这类节点的模板数据(使其显示在视图上),以及初始化相应的订阅者
Compile实现
页面的初始化
实现页面的初始化,即解析出v-text和v-model指令,并将data中的数据渲染到页面
解析各种指令成真正的html,解析和绑定dom
将页面上的数据变更同步到vue实例中
在input元素上绑定一个input事件,将data中的相应数据修改为input中的值
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
console.log([child])
self.compileElement(child, vm);
frag.append(child); // 将所有子节点添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /{{(.*)}}/;
//节点类型为元素(input元素这里)
if(node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for(var i = 0; i < attr.length; i++ ) {
if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name]= e.target.value;
});
new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
}
};
}
//节点类型为text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'nodeValue');
}
}
}
}
Observer实现
将vue实例中的数据渲染到页面上
当data中的数据发生更新的时候,绑定了该数据的元素会在页面上自动更新视图
用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
//添加订阅者watcher到主题对象Dep
if(Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if(newVal === val) return;
val = newVal;
console.log(val);
// 作为发布者发出通知
dep.notify();//通知后dep会循环调用各自的update方法更新视图
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
Watcher实现
vue实例中data数据变更 页面上数据同步变更
通过Compile获取dom的指令生成Watcher,将其通过到对应data的Observer,添加到指定的Dep中,将指令和data对应
function Watcher(vm, node, name, type) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.type = type;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function() {
this.get();
this.node[this.type] = this.value; // 订阅者执行相应操作
},
// 获取data的属性值
get: function() {
console.log(1)
this.value = this.vm[this.name]; //触发相应属性的get
}
}
Dep实现
为每个属性添加订阅者列表
在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
总结
首先为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。