MVVM框架
在讲MVVM框架的时候,就绕不开MVC框架
MVC框架
将整个前端页面分成View,Controller,Modal,视图上发生变化,通过Controller(控件)将响应传入到Model(数据源),由数据源改变View上面的数据。
但是由于MVC框架允许view和model直接俄通信,所以随着业务量的扩大,可能会出现很难处理的依赖关系,完全背离了开发所应该遵循的“开放封闭原则”。
MVVM详解
面对这个问题,MVVM框架就出现了,它与MVC框架的主要区别有两点:
1、实现数据与视图的分离
2、通过数据来驱动视图,开发者只需要关心数据变化,DOM操作被封装了。
数据就是简单的javascript对象,需要将数据绑定到模板上。监听视图的变化,视图变化后通知数据更新,数据更新会再次导致视图的变化!
VUE双向绑定原理
Vue的双向绑定主要通过compile(编译模板)、数据劫持、发布者-订阅者模式模式来实现的。初始化数据时通过Object.defineProperty来劫持各个属性的getter和setter。在编译模板时,把依赖数据的元素创建观察者,在get属性的时候,将watcher实例放入订阅列表中,在set数据的时候,notify所有的订阅者,触发订阅者的update方法来更新数据。达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。
Object.defineProperty
Object.defineProperty可以又两种方式在对象上直接定义一个属性,或者修改一个已经存在的属性的值。
方法1:属性描述符
let obj = {};
Object.defineProperty(obj, 'test', {
// 可配置
configurable: true,
// 可写
writable: true,
value: 'liuliu',
// 是否可遍历
enumerable: true
})
运行结果:
可以通过definePorperty方法给对象添加一个test属性,此属性configurable时,此属性可删除;writeable时,可以重新给此属性赋值。enumable时,此时行可以被Object.keys()等方法遍历。
方法2:存取描述符
let obj = {};
let val = null;
Object.defineProperty(obj, 'test', {
get() {
console.log('get value')
return val;
},
set(value) {
console.log('set value')
val = value;
}
})
由一对 getter、setter 函数功能来描述的属性。
注意:两种方法不可同时使用
vue数据劫持
index.html
mvvm.js
function Vue(options) {
this.$options = options;
let data = this._data = this.$options.data;
observe(data);
}
//观察对象 给对象添加defineProperty
function observer(data) {
for (let key in data) {
let val = data[key];
// 如果val不是基本数据类型,则需要继续劫持
if (val != null && typeof val === "object") {
observer(val);
}
Object.defineProperty(data, key, {
enumerable: true,
get() {
console.log('get data')
// 返回data[key]的值。
return val;
},
set(newval) {
console.log('set data');
// 如果数据发生变化
if (newval === val) {
return;
}
//将新值赋予val
val = newval;
// 新值如果不是基本数据类型,则需要继续
if (val != null && typeof val === "object") {
observer(val);
}
}
})
}
}
测试结果
在初始化数据的时候,我们通过observe函数来观察初始数据,设置数据的getter和setter来劫持数据。我们在获取a的值的时候,会执行其get方法,打印数据并返回a的值,在给属性赋值的时候执行set方法,如此这样我们就可以在get和set数据的时候做一些其它的操作。
数据代理
mvvm.js
function Vue(options) {
this.$options = options;
let data = this._data = this.$options.data;
observe(data);
// this 代理this._data
Object.keys(data).forEach(key =>{
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this._data[key];
},
set(value) {
this._data[key] = value;
}
})
})
}
运行结果
Vue在访问data数据的时候是使用this.a的方式而不是this._data.a,所以遍历data的属性,将其通过defineProperty的方法代理到Vue上面。在获取vue.a值的时候,get中返回vue._data.a的值,这时候会触发observe中对a属性get的劫持,返回this._data.a的值。在给vue.a进行赋值时,由于get的是vue._data.a的数值,则需要将新值set给vue._data.a。
通过数据代理,我们将vue._data中的数据代理到vue中。
编译模板
index.html
b的值为:{{a.b}}
c的值为:{{c}}
mvvm.js
function Vue(options) {
...
//和上文保持一致
// 编译模板
new Compile(options.el, this);
}
function Compile(el, vm) {
// 获取vue实例的根元素
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
// 将dom节点移动到内存中
while(child = vm.$el.firstChild) {
fragment.appendChild(child);
}
function replace(fragment) {
// fragement是一个类似数组结构
Array.from(fragment.childNodes).forEach(node =>{
// 获取节点的文本内容
let content = node.textContent;
// {{}}的正则
let reg = /\{\{(.*)\}\}/;
//如果node是文本节点且有需要编译的{{}}
if(node.nodeType === 3 && reg.test(content)) {
debugger
// 获取匹配正则表达式中的第一个匹配 (a.b) 并将其分割成字符数组[a,b]
let arr = RegExp.$1.split('.');
var val = vm;
// 获取vue.a.b的值
arr.forEach(key =>{
// 会劫持vue._data中的get方法来获取返回的数据
val = val[key];
})
// 把获取的数据替换掉模板
node.textContent = content.replace(/\{\{(.*)\}\}/, val);
}
// 如果当前结点还有子节点 则递归编译
if(node.childNodes) {
replace(node);
}
})
}
//编译vue实例的根节点
replace(fragment);
// 将在内存中的节点重新入到dom中
vm.$el.appendChild(fragment);
}
运行结果:
劫持并代理数据之后,我们开始编译文档中存在的模板。获取根节点app的文档元素,并将其移入到内存中,递归循环判断节点文本中的{{}}
,并将其替换为data中对应的数据。最后将替换完成的文档片段插入到dom中,从而完成编译。
观察者
mvvm.js
function Compile(el, vm) {
// 获取vue实例的根元素
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
// 将dom节点移动到内存中
while(child = vm.$el.firstChild) {
fragment.appendChild(child);
}
function replace(fragment) {
// fragement是一个类似数组结构
Array.from(fragment.childNodes).forEach(node =>{
// 获取节点的文本内容
let content = node.textContent;
// {{}}的正则
let reg = /\{\{(.*)\}\}/;
//如果node是文本节点且有需要编译的{{}}
if(node.nodeType === 3 && reg.test(content)) {
debugger
// 获取匹配正则表达式中的第一个匹配 (a.b) 并将其分割成字符数组[a,b]
let arr = RegExp.$1.split('.');
var val = vm;
// 获取vue.a.b的值
arr.forEach(key =>{
// 会劫持vue._data中的get方法来获取返回的数据
val = val[key];
})
// 添加watcher 当data中依赖的数据改变时,通过watcher的update方法更新到页面中去
new Watcher(vm, RegExp.$1, function(newVal){
node.textContent = content.replace(/\{\{(.*)\}\}/, newVal);
})
// 把获取的数据替换掉模板
node.textContent = content.replace(/\{\{(.*)\}\}/, val);
}
// 如果当前结点还有子节点 则递归编译
if(node.childNodes) {
replace(node);
}
})
}
//编译vue实例的根节点
replace(fragment);
// 将在内存中的节点重新入到dom中
vm.$el.appendChild(fragment);
}
// 观察者
function Watcher(vm, exp, fn) {
// 当前对象
this.vm = vm;
// 正则
this.exp = exp;
//回掉函数
this.fn = fn;
// 获取引用对应的值
let arr = this.exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key];
})
}
Watcher.prototype.update = function() {
// 获取data中的值
let val = this.vm;
let arr = this.exp.split('.');
arr.forEach(key =>{
val = val[key];
})
// 执行watcher的update方法,使更新的值渲染到文档中
this.fn(val);
}
观察者需要在依赖的数据该生改变时,执行wathcer的update方法,将新的数据渲染到文档中去。所以我们要给需要编译替换的元素添加Watcher实例,并将当前元素的data和表达式传入。更新值的时候可以根据这两个参数获取到最新的数据。
发布订阅
function Sub()
//发布订阅
function Dep() {
// subs中存储订阅实例
this.subs = [];
}
// 添加订阅,给依赖当前数据的实例的watcher添加到订阅列表中
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
}
// 发布信息函数
Dep.prototype.notify = function() {
// 依次执行订阅者的update方法使得订阅者也更新
this.subs.forEach(item => {
item.update();
})
}
function Watcher()
// 观察者
function Watcher(vm, exp, fn) {
// 当前对象
this.vm = vm;
// 正则
this.exp = exp;
//回掉函数
this.fn = fn;
//将当前实例绑定到Dep构造的属性上
Dep.target = this;
// 获取引用对应的值
let arr = this.exp.split('.');
let val = vm;
arr.forEach(key => {
// 获取data数据的时候 因为target不为空,所以会将当前实例放入val的订阅列表中
val = val[key];
})
// 循环完依赖 将target置空,以免将当前实例添加在非依赖的订阅列表中
Dep.target = null;
}
function observe()
//观察对象 给对象添加defineProperty
function observe(data) {
for(let key in data) {
let val = data[key];
let dep = new Dep();
//如果data的属性值还是对象,则递归做劫持
if (data !== null &&typeof val=== 'object') {
observe(val);
}
// 使用defineProperty的方式定义属性
Object.defineProperty(data, key, {
enumerable: true,
get() {
// 当target不为空时,则当前data为target的依赖, 所以将其添加到订阅列表中
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
// 如果旧值不等于新值 则赋值
if(val === newVal) {
return ;
}
// 在get数据的时候可以将新值返回
val = newVal;
// 如果新值为一个对象,则需要继续对属性做劫持
if(val !== null && typeof val === 'object') {
observe(val);
}
// 数据发生变化时,通知订阅者更新
dep.notify();
}
})
}
}
运行结果:
当获取编译模板时,生成Watcher实例,在观察者的构造方法中循环获取依赖数据的value,此时observe会get劫持,将当前观察实例放入订阅列表中。若模板依赖的数据发生改变,observe劫持set,将新值复制给当前属性,并通知subs中所有的依赖,使其执行update方法来进行更新。
双向绑定
index.html
b的值为:{{a.b}}
c的值为:{{c}}
function Compile()
function Compile(el, vm) {
// 获取vue实例的根元素
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
// 将dom节点移动到内存中
while(child = vm.$el.firstChild) {
fragment.appendChild(child);
}
function replace(fragment) {
// fragement是一个类似数组结构
Array.from(fragment.childNodes).forEach(node =>{
// 获取节点的文本内容
let content = node.textContent;
// {{}}的正则
let reg = /\{\{(.*)\}\}/;
//如果node是文本节点且有需要编译的{{}}
if(node.nodeType === 3 && reg.test(content)) {
// 获取匹配正则表达式中的第一个匹配 (a.b) 并将其分割成字符数组[a,b]
let arr = RegExp.$1.split('.');
var val = vm;
// 获取vue.a.b的值
arr.forEach(key =>{
// 会劫持vue._data中的get方法来获取返回的数据
val = val[key];
})
// 添加watcher 当data中依赖的数据改变时,通过watcher的update方法更新到页面中去
new Watcher(vm, RegExp.$1, function(newVal){
node.textContent = content.replace(/\{\{(.*)\}\}/, newVal);
})
// 把获取的数据替换掉模板
node.textContent = content.replace(/\{\{(.*)\}\}/, val);
}
//如果是元素节点
if(node.nodeType === 1) {
// 获取元素的所有属性
let attrs = node.attributes;
Array.from(attrs).forEach(attr =>{
//attr = 'v-model= "b" '
let name = attr.name; //name = v-model
let exp = attr.value //exp = b;
// 如果属性以v-开头
if (name.indexOf('v-') === 0) {
let val = vm;
exp.split('.').forEach(key =>{
val = val[key];
})
node.value = val;
// 订阅数据更新事件
new Watcher(vm, exp, function (newVal) {
node.value = newVal;
})
// 输入框变化时, 将值赋予到vm上
node.addEventListener('input', function (e) {
let newVal = e.target.value; //获取新值
// 触发observe的set
expr = exp.split('.');
expr.reduce((prev, next, index) => {
if(index === expr.length -1) {
prev[next] = newVal;
} else {
return prev[next];
}
}, vm)
})
}
})
}
// 如果当前结点还有子节点 则递归编译
if(node.childNodes) {
replace(node);
}
})
}
//编译vue实例的根节点
replace(fragment);
// 将在内存中的节点重新入到dom中
vm.$el.appendChild(fragment);
}
运行结果:
监听输入框的input事件,将新值复制给data, set会通知订阅者进行更新。直接改变data数据,劫持数据set的时候会通知依赖update,从而实现了数据的双向绑定。
计算属性
index.html
b的值为:{{a.b}}
c的值为:{{c}}
{{hello}}
mvvm.js
function Vue(options) {
this.$options = options;
let data = this._data = this.$options.data;
// 观察数据data
observe(data);
// this 代理this._data
Object.keys(data).forEach(key =>{
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this._data[key];
},
set(value) {
this._data[key] = value;
}
})
})
initComputed.call(this);
// 编译模板
new Compile(options.el, this);
}
// 初始化计算属性
function initComputed() {
debugger
let vm = this;
let computed = this.$options.computed;
for(k in computed) {
Object.defineProperty(vm, k, {
enumerable: true,
// 判断计算属性是个函数还是一个对象 hello() {} or hello: {get(){}, set() {}}
get: typeof computed[k] === 'function' ? computed[k] : computed[k].get,
set() {
}
})
运行结果:
盼盼计算属性是一个函数还是有get和set的对象。执行其方法,获取data的数据并返回。由于data中的数据都是在缓存中的,所有computed属性具有缓存依赖性。