实现方式:数据劫持结合发布者-订阅者模式
MVVM的实现核心:
- Observer监听器
劫持并监听data内的所有属性,如有变动,拿到最新值并且通知订阅者
- Watcher订阅者
收到属性的变动通知,执行指令绑定的回调函数,从而更新视图
起到桥梁作用
- Compiler解析器
对每个DOM节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
代码实现思路:
监听器Observer
- 遍历所有属性,进行响应式转化,做到监听所有属性的变化
function observer(data, vm) { // 遍历所有子属性
if (!data || typeof data !== 'object') {
return;
}
return new Observer(data); // run函数
}
Observer.prototype = {
run: function (data) {
var _this = this;
Object.keys(data).forEach(function (key) { // 遍历data中的所有属性
_this.convert(key, data[key]); // 将每个属性转化成可响应数据 即defineReactive
});
},
convert: function(key, value) {
this.defineReactive(this.data, key, value);
},
defineReactive: function(data, key, value) {
var obj = observer(value); //如果data中的属性是一个对象,通过递归方法,监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
return value;
},
set: function(newVal) {
if (value === newVal) {
return;
}
value = newVal;
obj = observer(value); // 新的值是object的话,进行监听
}
})
},
}
问答:
为什么 Vue 不支持 IE8 以及更低版本浏览器?
因为:Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性。
Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性,然后返回一个由属性名组成的数组。注意它同for…in一样不能保证属性按对象原来的顺序输出。
- 创建消息订阅器Dep:负责收集订阅者Watcher,当属性变化时,触发notify,再调用订阅者的update方法
Object.defineProperty(data, key, {
...
set: function(newVal) {
...
dep.notify(); // 如果数据变化,通知所有订阅者
}
})
function Dep() { // 属性订阅器
this.id = uid++;
this.subs = []; // 数组 存储属性 用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
}
Dep.prototype = {
addSub: function(sub) { // 负责向订阅器Dep中添加属性
this.subs.push(sub);
},
depend: function() {
Dep.target.addDep(this); // 添加订阅器 addDep为watcher中的方法
},
notify: function() { // 如果数据有变化 通知所有订阅者
this.subs.forEach(function(sub){
sub.update(); //更新属性
})
}
}
Dep.target = null; // 定义全局变量暂存watcher
- 给Dep添加订阅者:就必须要在闭包内操作,所以在getter里面动手脚。
Object.defineProperty(data, key, {
...
get: function() {
if(Dep.target) { // 用来区分是普通get还是收集依赖时的get 判断是否需要添加订阅者
dep.depend(); // 在这里添加一个订阅者
}
return value;
},
...
})
将Dep添加设置在getter()中,是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者;
在setter()中,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数
订阅者Watcher
- 初始化的时候需要将自己添加进订阅器Dep中
监听器Observer在get()中执行了添加订阅者Watcher的操作,所以只要在订阅者Watcher初始化时,触发对应的get函数(获取对应的属性值)即可完成添加订阅者操作。
function Watcher(vm, expFn, cb) {
this.vm = vm;
this.expFn = expFn;
this.cb = cb;
this.depIds = {};
// 此处为了触发属性的getter,从而在dep添加自己 即 将自己添加到订阅器的操作
this.value = this.get();
}
Watcher.prototype = {
...
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.getter.call(this.vm, this.vm); // 这里会触发属性的getter,从而添加订阅者 将自己添加到属性订阅器中
Dep.target = null; // 添加完毕后,释放闭包中的变量
return value; // 返回从订阅器中获取的属性最新值
},
addDep: function(dep) { // 添加订阅器的方法
if(!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
}
- 初始化Dep添加
因为只在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了
Watcher.prototype = {
...
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.getter.call(this.vm, this.vm); // 这里会强制触发属性的getter,从而添加订阅者 将自己添加到属性订阅器中
Dep.target = null; // 添加完毕后,释放闭包中的变量
return value; // 返回从订阅器中获取的属性最新值
},
...
}
- 定义自身的update方法(因为在Observer中调用了watcher的update方法)
Dep.prototype = {
...
notify: function() { // 如果数据有变化 通知所有订阅者
this.subs.forEach(function(sub){
sub.update(); //更新属性
})
}
}
Watcher.prototype = {
update: function() {
this.watchRun();
},
watchRun: function(){
var originalValue = this.value; // 更新前属性的值
var value = this.get(); // 更新后属性的值
if (originalValue !== value) {
this.value = value;
this.cb.call(this.vm, value, originalValue); // 属性发生变化时,更新属性
}
},
}
- 待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
解析器Compile
- 将根节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中
// 遍历解析过程涉及多次dom节点操作,当要向document中添加大量数据时,如果逐个添加这些新节点,因为每添加一个节点都会调用父节点的appendChild()方法产生一次回流和重绘,过程非常缓慢,
// 为了提高性能,可以使用一个文档碎片,把所有的新节点附加其上,
// 然后把文档碎片一次性添加到document中,减少了重新渲染的次数提高了性能。
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el); // 判断是否为元素节点
if(this.$el) {
this.$fragment = this.nodeToFragment(this.$el); // 复制原生节点
this.init(); // 初始化
this.$el.appendChild(this.$fragment); // 实现初始化的时候将数据渲染到视图上
}
}
Compile.prototype = {
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment(); // 创建元素
var childElement;
while(childElement = el.firstChild) { // 将原生节点拷贝到fragment 将el中的元素全部复制到fragment集中
fragment.appendChild(childElement);
}
return fragment;
},
init: function () {
this.compileElement(this.$fragment); // 解析fragment集的元素
},
};
childElement = el.firstChild表示:appendChild是把一个节点给"移"到flag上, 也就是说移动之后node里面就没有这个child节点了, 所以这样可以遍历node的所有孩子并把它们都移到文档碎片fragment上
- 扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定
本文只对带有 ‘{{变量}}’ 这种形式的指令进行处理
compileElement: function (el) {
var _this = this;
var child = el.childNodes;
[].slice.call(child).forEach(function (item) {
var text = item.textContent; // 获得元素的文本属性
var reg = /\{\{(.*)\}\}/; // 表达式文本
if(_this.isElementNode(item)) { // 判断是否为元素节点
_this.compile(item);
} else if (_this.isTextNode(item) && reg.test(text)){ // 判断是否是符合这种形式{{}}的指令
_this.compileText(item, RegExp.$1); // 与正则表达式匹配的第一个子匹配的字符串
}
if(item.childNodes && item.childNodes.length) { // 继续遍历子元素
_this.compileElement(item);
}
});
},
[].slice.call():将arguments对象的数组提出来转化为数组,arguments本身并不是数组而是对象
- 判断是否是元素节点
isElementNode: function (node) { // 判断是否为元素节点
return node.nodeType === 1;
},
// 解析指令
compile: function (node) {
var _this = this;
var nodeAttrs = node.attributes;
[].slice.call(nodeAttrs).forEach(function (item) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = item.name; // 本例中为 v-text
if(_this.isDirective(attrName)) { // 判断是否为指令 v-xxx
var value = item.value; // 本例中为 content
var dir = attrName.substring(2); // 提取属性的v-后面的关键字 本例中为 text
if(_this.isEventDirective(dir)) { // 事件指令 如 v-on:click
compileUtil.eventHandler(node, _this.$vm, value, dir)
} else {
// 普通指令 此处为v-model v-text
compileUtil[dir] && compileUtil[dir](node, _this.$vm, value); // 这里只处理了v-model
}
node.removeAttribute(attrName); // 移除已读取处理过的dom节点上设置的属性
}
})
},
isDirective: function (attrName) { // 判断是否为指令
return attrName.indexOf('v-') === 0;
},
isEventDirective: function (tmp) { // 判断是否为事件指令
return tmp.indexOf('on') === 0;
},
eventHandler: function (node, vm, exp, tmp) { // 事件指令处理方法
var eventType = tmp.split(':')[1]; // 事件指令绑定的方法名 v-on:click中的click
var fun = vm.$options.methods && vm.$options.methods[exp]; // 绑定的方法
if(eventType && fun) {
node.addEventListener(eventType, fun.bind(vm), false); // 给元素添加监听事件,即绑定的事件
}
},
var compileUtil = {
text: function (node, vm, exp) { // 文本解析
this.bind(node, vm, exp, 'text')
},
model: function (node, vm, value) { // model指令解析
this.bind(node, vm, value, 'model');
var _this = this;
var val = this.nfuva // 读取属性的值
node.addEventListener('input', function (e) { // 为input输入框添加监听事件,value值发生变化时触发
var newValue = e.target.value; //
if(val === newValue) {
return ;
}
_this._setVMVal(vm, value, newValue);
val = newValue;
})
},
}
- 判断是否是文本内容
isTextNode: function (node) { // 判断是否为文本内容
return node.nodeType === 3;
}
compileText: function (node, exp) { // 解析文本
compileUtil.text(node, this.$vm, exp);
},
var compileUtil = {
text: function (node, vm, exp) { // 文本解析
this.bind(node, vm, exp, 'text')
},
bind: function (node, vm, exp, tmp) {
var updaterFn = updater[tmp + 'Updater'];
// 第一次初始化视图
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function (value, originalValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, originalValue);
})
},
}
// 更新函数
var updater = {
textUpdater: function (node, value) { // 文本内容更新
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function (node, value, originValue) { // model指令绑定的属性值更新
node.value = typeof value == 'undefined' ? '' : value;
}
};
双向数据绑定
data到view总结:data:msg—>{{msg}}
data属性变化–>触发set–>dep.notify()通知所有订阅者watcher–>Dep调用了所有Watcher的update–>data的值赋给了view中的节点
view到data总结:input输入框—>data:inputValue
compile.js
给view的节点添加事件—>如 input事件触发时,把e.target.value赋值给vue对象里面的data,以此来实现view的变化同步到data上
github完整可运行代码https://github.com/MingleJia/vue-data-binding
参考链接:
https://www.cnblogs.com/libin-1/p/6893712.html
https://www.jianshu.com/p/c2fa36835d77
https://github.com/DMQ/mvvm
https://segmentfault.com/a/1190000007741904