Vue双向数据绑定(渣渣版)
1. 双向数据绑定的基本过程
双向数据绑定一般来说,常见的我们可以知道是使用object.defineProperty结合发布订阅者模式来实现的。理解双向数据绑定原理的难点一般是在于这个发布订阅者模式,对于对象属性特性的拦截器一般来说不会有多大的理解困难。只是在vue3.0版本中针对属性拦截器的一些缺点比如数组的监控不足,是使用了proxy代理来进行拦截。这里不展开讲,只是围绕vue讲一下我自己对双向数据绑定过程的理解。
首先先看一下双向数据绑定的使用,如下所示:
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
- 文本类型的单向绑定,这里的{{title}}、{{name}}属于文本类的绑定,这是一种单向的数据绑定,即数据改变会更新到视图,但是视图无法执行更新操作。
- input中的v-model绑定,这个绑定是经常在开发中使用的用户表单控件,最典型的双向绑定的模式:
- 视图到数据的绑定:在input元素对象上加入了DOM2级监听事件addEventListener, 然后当用户在视图层更改数据时就会触发input事件(在用户输入时触发):
- 数据到视图的绑定:
(1)通过 Object.defineProperty(obj, name, handler配置对象) 来对vue实例中的data属性特性进行监听,一旦出现更改就会触发setter钩子函数(触发钩子函数的作用就是为了达到监听的作用)
(2)钩子函数的触发达到了监听的作用,但是监听的目的是为了告知使用到这个data.name属性的DOM节点去更新对应的视图,如何告知就使用一个watcher来告知双向数据绑定的比较难理解的点就是这个抽象的watcher。可以这么理解一个使用data.name数据的DOM节点就需要一个watcher观察者,这个观察者起到一个连接的作用:1. 连接DOM节点:这个观察者Wacher里面有一个update函数,函数内部执行callback--更新视图函数(获取DOM节点,更改input节点的value值)。2.连接监听器Object.defineProperty(),通过监听器的钩子函数内部触发这个watcher的update函数,执行内部的视图渲染。
node.addEventListener('input', function(e) {
// 这里双向数据绑定中从视图到数据主要是通过事件的触发来执行的
// (2)从视图到数据的更新---
// 1.通过对input标签进行事件的监听来执行,主要是通过新输入的值然后替换为vue实例中的data属性中值
// 注:当替换了data属性值的同时又触发了监听器observer,这个时候又会执行一次数据到视图的更新,这里
//虽然这个input对象已经在视图上进行了更改,但是其他绑定这个data属性的试图把并没有完成更新,因此每一次执行
//视图到属性的更新的时候都必然会触发一次数据到视图的更新过程!!!
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
- 事件的监听绑定,这里与上面不同的是这里绑定的不是data属性而是methods属性
<script type="text/javascript">
new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: '11'
},
methods: {
clickMe: function () {
this.title = 'hello world';
}
},
mounted: function () {
window.setTimeout(() => {
this.title = '你好';
}, 1000);
}
});
</script>
2. 双向数据绑定的基本组成
1. 对象data的属性监听器--Observer
function Observer(data) {
this.data = data;
this.walk(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();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
2. 订阅通知器--Watcher
function Watcher(vm, exp, cb) {//这里vm为vue实例,exp为data中的属性名,cb为对应的获取dom节点更改视图值的函数
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
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, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己,因为每一个节点都要使用watcehr,所以每次使用完以后需要释放自己
return value;
}
};
3. 视图渲染器--Compiler
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el); //querySelector(el) 指匹配指定选择器的第一个元素,此时el为元素节点。如果你需要返回所有的元素,使用 querySelectorAll() 方法替代。
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元素不存在');
}
},
nodeToFragment: function (el) {
//遍历这个挂载点看看这个挂载点有多少子节点,都放在文档片段中
var fragment = document.createDocumentFragment(); // 创建一个新的空白的文档片段
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
compileElement: function (el) {
// 1.获取文档节点的所有子节点,得到的是一个类数组对象
// 2.然后对这个childNode类数组变换为数组然后进行遍历检查各个子节点的节点类型nodeType进行不同的渲染函数
// 3.进行渲染:
// 3.1如果nodeType值为1:表示该子节点为元素节点(元素节点例如<div class>),重点掌握对属性的解析
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent;
// 如果是节点对象类型那么就可能涉及双向数据绑定中的两个方向,即双向数据绑定的全部过程包括监听器、观察通知器以及compile渲染器
// 如果是{{}}类型那么就只有数据到视图的更新,此时建立对应的new Watcher然后通过watcher触发updateText函数来更新视图
if (self.isElementNode(node)) {
self.compile(node);
} else if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes; // 重点掌握对属性的解析
// 1.返回当前节点的所有属性节点组成一个类数组对象
// 2.然后进行遍历,遍历的每一项为属性节点对象(具体参见属性节点对象的相关方法: name、value)
// 3.根据每个属性节点对象对应的属性名称attrName(包括自定义的属性也在里面,因此可以解析到v-model)
// 4.然后根据属性节点名称判断是不是vue的指令v-,如果是进入vue属性的相关操作:
// 4.1 通过属性名name来截取第二个以后的字符串获取真正的属性名actual_attribute,然后在通过value获取属性值
// 4.2 将真正的属性名按照类型的不同进行分类:时间类on或者model绑定类等,如果为双向绑定命令属性model则进入下一步
// 4.2.1 解析model属性compileModel(node节点对象, self.vm-vue实例, exp属性值, dir真正的属性名)
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);
//传入拥有v-model属性的节点对象,vue实例对象,v-model的属性值exp,
}
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, dir) {
//(属性节点对象,vue实例,属性名,属性值)
// 开始对
var self = this;
var val = this.vm[exp];//v-mode的属性值message对应的是在vue实例中的data对象的属性名message,然后获取真正的属性值
this.modelUpdater(node, val);
//这样就可以通过v-mode的属性值对应vue中data属性名的属性值,然后将
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value);
//在input元素中通过js操作动态的修改value值
//这里新建了一个watcher来负责监视这个属性,如果data中这个属性值变化那么就会触发
//watcher中的更新函数,更新函数就会将新的值传入到这个回调中,通过更改input的value来更新视图
//到这里就已经是实现了双向数据绑定中的数据到视图的更新
//小结:简单说一下
// (1) 数据到视图的更新---
// 1.首先建立监听器observer,作用是如果更新了data的数据就可以通知watcher来执行更新函数。
// 2.然后建立观察通知器watcher,作用是监听器拦截到更改从操作之后,通过遍历Dep中的watcher更新函数
//来执行视图更新函数,在这里每一个watcher的回调函数中都会自动对应更新自己的节点node。
// 3.构建渲染解析器compiler,因为观察者之所以能观察到对应的与data属性绑定的节点,主要是通过解析器
//解析出v-model属性,然后进入对应节点对象v-model属性的渲染器(在对应的渲染函数中进行new watcher的创建)
//这样在执行渲染时才能传入对应的节点对象input,执行渲染函数node.value=newValue进而更新视图中input输入框的值
// 注:根据模板解析中的节点中属性是否为v-model来确定是否要建立对应的new Watcher
});
node.addEventListener('input', function(e) {
// 这里双向数据绑定中从视图到数据主要是通过事件的触发来执行的
// (2)从视图到数据的更新---
// 1.通过对input标签进行事件的监听来执行,主要是通过新输入的值然后替换为vue实例中的data属性中值
// 注:当替换了data属性值的同时又触发了监听器observer,这个时候又会执行一次数据到视图的更新,这里
//虽然这个input对象已经在视图上进行了更改,但是其他绑定这个data属性的试图把并没有完成更新,因此每一次执行
//视图到属性的更新的时候都必然会触发一次数据到视图的更新过程!!!
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, oldValue) {
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;
}
}
4. vue构造函数
function SelfVue (options) {
var self = this;
this.data = options.data;
this.methods = options.methods;
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key);
});
observe(this.data);
new Compile(options.el, this);
options.mounted.call(this); // 所有事情处理好后执行mounted函数
}
SelfVue.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function getter () {
return self.data[key];
},
set: function setter (newVal) {
self.data[key] = newVal;
}
});
}
}