什么是双向绑定
双向绑定是指,数据和视图的同步变动
为什么试用双向绑定
单向数据绑定,数据的流动方向繁多,不便追踪,然而当 data 数据变动时,就须要将之前的 html 代码去掉,把新的数据和模板插入到文档中。
双向数据绑定刚好解决了这个问题,当数据变动时,页面同时也会变动,只是不太容易跟踪数据源
如何实现双向绑定
vue 是通过 Object.defineProperty
和联合公布订阅者模式
实现的
接下来咱们用代码来实现
- vue 的模板文件
<div id="app">
<input type="text" v-model="text" />
{{text}}
</div>
<script>
new Vue({
data: {text: "hello world",},
el: "app",
});
</script>
-
vue 模板文件浏览器是不能辨认的,咱们须要将 v -model、{{}}等这样的语法转换成浏览器能够辨认的语法,让 text 值能够回显的页面上
文档碎片的作用:能够须要插入的节点,放到文档碎片上,最初一次性插入到文档中
// 创立 Vue 函数 初始化参数
function Vue(options) {
this.data = options.data;
this.el = options.el;
// nodeToFragment 函数 返回浏览器能够辨认的 dom 元素
let dom = nodeToFragment(document.getElementById(this.el), this);
document.getElementById(this.el).appendChild(dom);
}
/**
获取生成的 dom 元素
node: app dom 元素
vm vue 对象
**/
function nodeToFragment(node, vm){
// 创立文档碎片
let fragment = document.createDocumentFragment();
while (node.firstChild) {
// 如果子元素,则编译其内容
compile(node.firstChild, vm);
// 将编译完的内容插入到模板碎片中,每插入一个元素,node 对应的删除相应的元素(appendChild 的作用)fragment.appendChild(node.firstChild);
}
return fragment;
}
// 编译内容
function compile(node, vm) {switch (node.nodeType) {
// 元素节点
case 1:
let atrr = node.attributes;
for (let i = 0; i < atrr.length; i++) {if (atrr[i].nodeName === "v-model") {let name = atrr[i].nodeValue;
// 监听键盘,每次扭转 html 中的值时,对应的 data 值也会变动
node.addEventListener("input", function (e) {vm.data[name] = e.target.value;
});
// 将 data 的值,赋值给 input
node.value = vm.data[name];
}
}
break;
// 文本节点
case 3:
// 正则匹配{{}}
let reg = /\{\{(.*)\}\}/;
if (reg.test(node.nodeValue)) {
let name = RegExp.$1;
// 将 data 的值,赋值给文本节点
node.nodeValue = vm.data[name];
}
break;
default:
break;
}
}
- 这个时候就要引入观察者订阅者模式,订阅者是指用到数据的中央,观察者是值察看数据的变动,而后告诉订阅者
订阅者:
function Watcher(vm, node, name) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {update() {
// 将 data 的值,更新视图
this.node.nodeValue = this.vm.data[this.name];
},
};
用 dep 寄存多个订阅者:
function Dep() {this.subs = [];
}
Dep.prototype = {
// 寄存订阅者,每个 sub 都是一个订阅者
addSub(sub) {this.subs.push(sub);
},
// 触发订阅者
notify() {this.subs.forEach((item) => {item.update();
});
},
};
观察者:用到 data 数据时,触发 get 办法,将观察者存到 dep 中,当给 data 赋值时,触发 set 办法,从而触发 dep 的 notify 办法,更新所有用到这个数据的视图
// 观察者,进行察看数据变动
function observe(obj) {for (const key in obj) {defaultReactive(obj, key, obj[key]);
}
}
// 用 Object.defineProperty 监听每个 data 值的变动
function defaultReactive(obj, key, val) {
// 每个属性都有一个 dep
let dep = new Dep();
Object.defineProperty(obj, key, {get: function () {if (Dep.target) {dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {if (newVal == val) {return;}
val = newVal;
// 数据一旦变动,就要告诉扭转试图,及要通知订阅者
dep.notify();},
});
}
-
当观察者和订阅者曾经实现后,咱们须要批改一些模板编译的内容, 文本 {{text}} 用到了 data 值,即是一个订阅者
let reg = /\{\{(.*)\}\}/; if (reg.test(node.nodeValue)) { let name = RegExp.$1; // node.nodeValue = vm.data[name]; new Watcher(vm, node, name); }
观察者一开始就要初始化的
function Vue(options) {
this.data = options.data;
this.el = options.el;
observe(this.data);
let dom = nodeToFragment(document.getElementById(this.el), this);
document.getElementById(this.el).appendChild(dom);
}