在前面的文章中我们说过,Vue的设计模式为MVVM设计模式,因此数据会通过VM来进行双端之间的交换,这种交换就需要两边的数据都要与VM绑定。我们就来盘一下数据双向绑定的原理以及简单实现。
基本概念
数据双向绑定是建立在响应式的基础之上的。我们在上一篇文章中提到了一个updateView函数和dep.notify函数。但是里面却是空的,无法进行真正对UI的修改。我们先来看一个简单的UI修改的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<input type="text" id="textInput">
<span id="textSpan"></span>
<script>
let obj = {},
textInput = document.querySelector('#textInput'),
textSpan = document.querySelector('#textSpan');
Object.defineProperty(obj,'key', {
set: function (newValue) {
textInput.value = newValue;
textSpan.innerHTML = newValue;
}
});
textInput.addEventListener('keyup', function (e) {
obj.key = e.target.value;
});
</script>
</body>
</html>
我们在输入框中的文字会实时反映到旁边。这样就能真正对UI造成影响了。但是毕竟Vue使用的并不是这种简单的绑定,我们想让数据绑定和Vue本身那样简单自然。
虚拟DOM的创建
如果有很多的元素用了同一份数据,那么当我们修改的时候可能无法一下全部修改,因此可能会导致肉眼上看出问题。因此我们需要一个虚拟DOM树来进行预处理。JavaScript原生提供了一个叫做DocumentFragment的东西可以完成上述操作。Vue在实际工作中,就是会把关联的所有子元素劫持到DocumentFragment中,修改完毕后再进行分发。
绑定后进行响应式处理
我们使用之前写过的响应式框架,就可以通过getter和setter来进行响应式的操作。总有人分不清响应式和双向绑定。双向绑定是实现MVVM设计模式的一种方法,而响应式是提高数据处理速度的方法。因此这两者不是一个事情。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>forvue</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text"> <br>
{{ text }} <br>
{{ text }}
</div>
<script>
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
});
}
function defineReactive(obj, key, val) {
let dep = new Dep();
// 响应式的数据绑定
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者watcher到主题对象Dep
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
} else {
val = newVal;
// 作为发布者发出通知
dep.notify();
}
}
});
}
function nodeToFragment(node, vm) {
let flag = document.createDocumentFragment();
let child;
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child); // 将子节点劫持到文档片段中
}
return flag;
}
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/;
// 节点类型为元素
if (node.nodeType === 1) {
let attr = node.attributes;
// 解析属性
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
let name = attr[i].nodeValue; // 获取v-model绑定的属性名
node.addEventListener('input', function (e) {
// 给相应的data属性赋值,进而触发属性的set方法
vm[name] = e.target.value;
})
node.value = vm[name]; // 将data的值赋值给该node
node.removeAttribute('v-model');
}
}
}
// 节点类型为text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋值给该node
new Watcher(vm, node, name);
}
}
}
function Watcher(vm, node, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
this.node.nodeValue = this.value;
},
// 获取data中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的get
}
}
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
}
function Vue(options) {
this.data = options.data;
let data = this.data;
observe(data, this);
let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将dom返回到app中。
document.getElementById(id).appendChild(dom);
}
let vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>