本章主要讲诉发布-订阅模式关于Vue数据双向绑定原理的应用,全程大概耗时10分钟。
Vue是一个MVVM模式的框架,即Model-View-ViewModel
数据双向绑定,用兵法就说就是敌不动我不动,敌一动我跟着动。也就是说用户对View视图进行更新时,Model数据也会跟着修改;而我们修改Model数据时,View视图也一样会跟着修改。而其原理,就是利用了发布-订阅模式+数据劫持实现的。我们先来说一下什么是数据劫持?
数据劫持,顾名思义,就是在操作数据的时候,劫持这个操作顺便做一些我们想做的事情。
在Vue3.0之前,数据劫持是利用了Object.defineProperty来劫持对象属性的setter和getter操作来实现的。getter就是获取某个属性,setter就是设置某个属性。看下下面的代码实例:
var people = {
name:'pjj'
}
Object.keys(people).forEach(function(key) {
Object.defineProperty(people, key, {
get:function(){
console.log('获取属性时劫持触发这个console.log');
},
set:function(){
console.log('设置属性时劫持触发这个console.log');
}
})
})
people.name; //控制台会打印出 “获取属性时劫持触发这个console.log”
people.name = 'panjj'; //控制台会打印出 "设置属性时劫持触发这个console.log"
在Vue3.0,数据劫持改成了由Proxy来实现
使用Proxy代替Object.defindProprety的很大原因是因为原先的数据劫持,如果属性值为复杂类型的数据,则需要进行深度遍历,没办法直接监听。Proxy是直接监听整个对象的,简单很多,但是是ES6的语法,所以兼容性不是很好。
let people = {
name: 'pjj'
}
let handler = {
get: function(target, key) {
console.log('获取属性时劫持触发这个console.log');
return key in target ? target[key] : 'newKey';
},
set: function(target, key, newVal) {
let res = Reflect.set(target, key, newVal);
console.log('设置属性时劫持触发这个console.log');
return target[key] = newVal;
}
}
let p = new Proxy(obj, handler);
//target指的是要被Proxy包装的任意目标对象
//handler是一个对象,这个对象的属性是一些执行行为的函数
p.name = 'panjj'; //控制台会打印出 “获取属性时劫持触发这个console.log”
console.log(p.age); //控制台会打印出 “设置属性时劫持触发这个console.log”
从上面的两段代码,我们可以看出,其实数据劫持就是劫持了数据属性的get和set,让数据的属性在被获取get或者设置set时做出额外的操作。
基于这一点,只要我们把这个额外操作的用途用来监听数据和更新视图,其实就可以去实现数据->视图的更新了。
而视图->数据的更新,其实可以利用一些监听事件去实现,例如像input事件就可以实现了。
Observer和Dep
//用来劫持并监听所有属性,如果有变动的,就通知订阅者。
function Observer(data) {
this.data = data;
this.walk(data); // 遍历data的每个属性,进行数据劫持
}
Observer.prototype = {
walk: function(data) {
var self = this;
//这里是通过对一个对象进行遍历,对这个对象的所有属性都进行监听
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
//数据劫持
var dep = new Dep();
// 递归遍历所有子属性
Object.defineProperty(data, key, {
get: function getter() {
if (Dep.target) {
// 在这里添加一个订阅者,Dep.target????
dep.addSub(Dep.target);
}
return val;
},
// setter,如果对一个对象属性值改变,就会触发setter中的dep.notify(),通知watcher(订阅者)数据变更,执行对应订阅者的更新函数,来更新视图。
set: function setter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
dep.notify();
}
});
}
};
// 返回实例化的对象
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数
function Dep() {
this.subs = []; //存储订阅者的subs
}
Dep.prototype = {
// 添加订阅
addSub: function(sub) {
this.subs.push(sub);
},
// 通知订阅者数据变更
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
Watcher
//可以收到属性的变化通知并执行相应的函数,从而更新视图。
function Watcher(vm, exp, cb) {
this.vm = vm; // vm 就是一个new Vue对象
this.exp = exp; // exp就是v-model或者v-on等等绑定的属性,例如v-modle='name',exp就是name
this.cb = cb; // cb就是Watcher绑定的更新函数
this.value = this.get(); // 这里的value也就是下面的22行的value,是为了方便下面进行update时做新旧值的对比
}
Watcher.prototype = {
update: function() {
var value = this.vm.data[this.exp]; // 新value
var oldVal = this.value; // 原value
if (value !== oldVal) {
// 不相等才进行更新
this.value = value;
this.cb.call(this.vm, value, oldVal); // call调用更新函数cb进行更新
}
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value);
}
},
get: function() {
Dep.target = this; // 这里让Dep.terget指向了自己(一个watcher)
var value = this.vm.data[this.exp]; // 这里this.vm.data[this.exp]也就是调用了上面例子中data的name,从而触发object.dedefineProperty中的get函数,把watcher添加到订阅器中
Dep.target = null; // 释放自己
return value;
}
};
Compile
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el); // 绑定的根元素
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
},
// 将绑定的dom元素整个添加到fragment元素
nodeToFragment: function(el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
// 解析element
compileElement: function(el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/; // 通过正则来获取胡子语法{}里面的data
var text = node.textContent;
// 如果是元素节点
if (self.isElementNode(node)) {
self.compile(node);
// 如果是文本节点
} else if (self.isTextNode(node) && reg.test(text)) {
//第 0 个元素是与正则表达式相匹配的文本 reg.exec(text)[0] 为 '{
{data}}'
//第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本 reg.exec(text)[1]为'data'
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
// 如果是指令
compile: function(node) {
var nodeAttrs = node.attributes;
var self = this;
Array.prototype.forEach.call(nodeAttrs, function(attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
if (self.isEventDirective(dir)) {
// 事件指令
self.compileEvent(node, self.vm, exp, dir); //绑定监听事件
} else {
// v-model 指令
self.compileModel(node, self.vm, exp, dir);
}
node.removeAttribute(attrName);
}
});
},
// 如果是{}
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, function(value) {
self.updateText(node, value);
});
},
//绑定监听事件
compileEvent: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1];
var cb = vm.methods && vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
},
compileModel: function(node, vm, exp) {
var self = this;
var val = this.vm[exp];
this.modelUpdater(node, val); // 完成挂载,{
{ }}中的值被渲染为data中的值
new Watcher(this.vm, exp, function(value) {
self.modelUpdater(node, value);
});
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
},
updateText: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function(node, value) {
node.value = typeof value == 'undefined' ? '' : value;
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on:') === 0;
},
isElementNode: function(node) {
return node.nodeType == 1;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
};