什么是双向绑定
双向绑定是指,数据和视图的同步变动
为什么试用双向绑定
单向数据绑定,数据的流动方向繁多,不便追踪,然而当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); }