Vue和React的兴起,MVVM模式已经成为主流开发思想,那这种模式的实现原理是什么?双向数据绑定是怎样工作的?发布订阅是什么?本文以Vue的设计思想带你解开这些迷团
Object.defineProperty()
Vue
是不支持IE8以下的浏览器,因为它使用了IE8无法模拟的ECMAScript5
特性:Object.defineProperty()
通常我们以字面量的方式定义一个对象,这种方式的属性是不存在get
和set
方法的,并且对象的属性是可以随意更改或删除
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。Vue就是用了这个方法进行数据劫持,在把数据挂载到Vue实例上
var o = {}; // 创建一个新对象
// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
value : 37,
writable : true,
enumerable : true,
configurable : true
});
// 对象o拥有了属性a,值为37
// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
get : function(){
return bValue;
},
set : function(newValue){
bValue = newValue;
},
enumerable : true,
configurable : true
});
o.b = 38;
// 对象o拥有了属性b,值为38
// o.b的值现在总是与bValue相同,除非重新定义o.b
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
复制代码
更详细的Object.defineProperty()
解释,猛戳
发布订阅
先说一下发布订阅模式和观察者模式有什么区别
发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式
发布订阅模式多了个事件通道在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应
通俗点说:
A告诉B去做三件事(买鞋、买裤子、买领带),每件事做完都要告诉A,
B买到鞋告诉A“鞋买到了”,A收到消息后发了个朋友圈;
B买到裤子后告诉A“裤子买到了”,A收到消息后发了个微博;
B买到领带后告诉A“领带买到了”,A收到消息后来了个自拍;
通俗点说:
N个人关注了A的公众号 (订阅)
A写好了文章提交到微信公众号平台 (发布)
微信公众号平台推送到了这N个人的微信客户端 (广播)
所有的订阅者接到消息后可以选择自己的动作阅读/忽略/分享
Object.defineProperty()
、
发布订阅
实现的双向数据绑定
MVVM
先看一下MVVM的原理,接下来我们根据这个图一步一步深入
Vue
的规则,自己写的
MyMVVM
<div id="app">
<p>a的值{{a.a}}</p>
<div>b的值{{b}}</div>
<input type="text" v-model="b">
{{hello}}
</div>
复制代码
let myMVVM = new MyMVVM({
el:"#app",
data:{
a:{a:"a"},
b:"是b"
}
})
复制代码
new MyMVVM()
实例化的时候MyMVVM实例挂载了传入的data
function MyMVVM(options = {}) {
this.$options = options; // 把所有属性挂载在$options
let data = this._data = this.$options.data;
// this 代理了this._data
for(let key in data){
Object.defineProperty(this,key,{
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
复制代码
Compile编译模板
function Compile(el,vm) {
// el表示替换的范围
// DOM中的节点塞入fragment时,原节点会被删除
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
while(child = vm.$el.firstChild){ // 获取到的元素节点 塞入fragment 在内存中操作
fragment.appendChild(child)
}
replace(fragment);
function replace(fragment){
Array.from(fragment.childNodes).forEach(function (node) { // 类数组转换为数组,循环
let text = node.textContent;
let reg = /\{\{(.*)\}\}/;
// 数据渲染视图
if(node.nodeType === 3 && reg.test(text)){ // 文本节点
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(function (k) { // 取this.a.a / this.b
val = val[k]
});
// 替换
node.textContent = text.replace(reg,val) // 替换模板
}
// 视图更新数据,数据再渲染视图
if(node.nodeType === 1){ // DOM节点 输入内容时数据一起更新
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(function (attr) {
let name = attr.name;
let exp = attr.value;
if(name.indexOf("v-") === 0){ // 带有v-指令的DOM节点
node.value = vm[exp];
}
node.addEventListener("input",function (e) {
let newVal = e.target.value;
vm[exp] = newVal // 调用VM上data的set方法跟新视图数据
})
})
}
if(node.childNodes){ // 节点深度递归
replace(node)
}
});
}
vm.$el.appendChild(fragment)
}
复制代码
Observer 数据劫持
数据劫持的过程就是把传递给实例的对象通过Object.defineProperty()
重新定义属性,这样就拥有了get
和set
方法,便于我们后续观察数据的变化
vue
特点是不能新增不存在的属性,因为不存在的属性在数据劫持的时候没法重新定义,也就不能增加get
和set
// 观察对象给对象增加ObjectDefineProperty
function Observe(data) {
for(let key in data){ // 把data属性通过Object.defineProperty()的方式 定义属性
let val = data[key];
Object.defineProperty(data,key,{
enumerable:true,
get(){
return val
},
set(newVal){
if(val === newVal) {
return;
}
val = newVal;
}
})
}
}
复制代码
Watcher
Watcher是一个类,通过这个类创建的实例都有update方法,用来执行数据发生变化后的更新动作
function Watcher(vm,exp,fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.target = this;
let val = vm;
let arr = exp.split('.');
arr.forEach(function (k) { // 目的是为了触发取值时的get方法,get方法中把watcher添加到队列里
val = val[k]
});
Dep.target = null; // 添加成功后target置为null
}
Watcher.prototype.update = function () {
let val =this.vm;
let arr = this.exp.split('.');
arr.forEach(function (k) {
val = val[k]
});
this.fn(val);
};
复制代码
发布订阅
先有订阅再有发布
function Dep() {
this.subs = []; // 存放订阅队列
}
Dep.prototype.addSub = function (sub) { // 往容器中存储订阅信息
this.subs.push(sub)
};
Dep.prototype.notify = function () { // 发布
this.subs.forEach(sub => sub.update())
};
复制代码
总结
我们完成了MVVM框架,正确的调用组合这些方法就能实现数据的双向绑定了,源码请参考这里