实现数据双向绑定
当前背景,只是input改变改变model也就是data中的数据,实现了单向的绑定,如何通过model,data的数据改变来改变视图view呢?
采用 发布/订阅者模式
5 创建一个订阅器Dep
6 创建一个订阅者Watcher(可以接收信息,更新渲染自身和添加进订阅器),会自调用自身的方法update,update的方法会对view层进行赋值渲染
6.1 watcher中的update方法会触发get方法从而会触发监听器中的get方法,此时会往订阅器添加订阅者
3.2.1 判断节点类型,对相对应的节点进行赋值 所以3.4步骤在此废弃
当 observe 监听器中监听数据改变时,调用订阅器的 notify 方法遍历通知所有订阅者
在实现双向绑定之前要先学习一下“订阅/发布模式”。订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候观察者都会得到通知。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作
还有个问题,订阅器什么时候初始化创建的呢?
vue构造函数中初始化便会对data中所有数据进行初始化遍历监听,监听的时候便会进行初始化的订阅器的创建,然后到代码片段nodeToFragment中的指令解析compile,如果遇到vue中的指令,便会创建一个订阅者,订阅者的初始化会将相对应的数据渲染到view视图层中。
这里我当时还有一个疑惑,创建订阅者初始化会调用监听器的方法,那不就再次进行监听,创建一个订阅器了吗?
然而不是,经过打点测试,创建订阅者watcher虽然是进入了 observe监听器,但是他只是促发里面的 get 方法,仅此而已!🤹
那我又要问了,dep订阅器你是怎么理解的呢?🖐

dep 是 Dep构造函数的实例化,对vm中的数据进行遍历,一个数据便是一个订阅器,监管他下面的所有订阅者(就是视图层引用绑定的数据)。

三个数据对应三个订阅器
相对应的订阅者
个人理解 dep 相当于 new Dep 相当于闭包 数据保存在内存当中。 当获取的时候触发相对应的数据监听获取相对应的 dep订阅器。
/* * vue 的监听器 * @param {Object} obj vm.data * @param {Object} vm */ function observe(obj, vm) { console.log(obj,vm); Object.keys(obj).forEach(key => { //对 vm.data 进行遍历 let dep = new Dep(); Object.defineProperty(vm, key, { enumerable: true, // 当 enumerable 为 true 时,该属性才能出现在对象的枚举属性中 configurable: true, // 当 configurable 为 true 时,该属性描述符才能够被改变 get() { // dep新建时 Dep.target 不为空 if (Dep.target) dep.addSub(Dep.target); // 向订阅器添加订阅者 Dep.target 相当于 Watcher return obj[key]; }, set(newVal) { if (obj[key] === newVal) return; obj[key] = newVal; //属性被修改是通知订阅者变更,订阅器作为发布者收到通知推送给订阅者,订阅者收到消息回调 dep.notify(); } }) }) } /** * 使用documentfragment处理节点速度和性能要高于直接操作dom。vue编译的时候,就是将挂载目标的所有子节点劫持到documentfragment中,经过处理后再将documentfragment整体返回到挂载目标中。 */ // DocumentFragment(文档片段,节点容器) 劫持vue挂载的所有节点 function nodeToFragment(node, vm) { let fragment = document.createDocumentFragment(); //创建代码片段 let child while (child = node.firstChild) { compile(child, vm) //appendChild 会删掉所有原来的节点 fragment.appendChild(child) //改变原节点 } return fragment } //解析指令和赋值 //做用对框架的指令进行双向绑定(数据响应,和视图更新渲染) function compile(node, vm) { //这里讲解一下,nodeType等于1说明节点是标签,所有解析标签所有的属性; nodeType等于3说明节点是文本,可以操作文本的文本名和值(node.nodeName,node.nodeValue) if (node.nodeType === 1) { let attr = node.attributes // 获取所有属性 // 解析属性 for (var i in attr) { if (attr.hasOwnProperty(i)) { if (attr[i].nodeName === 'v-model') { var name = attr[i].nodeValue node.addEventListener('input', function (event) { // 给相应的data属性赋值,进而触发该属性的set方法 //注意这里是给vm一级下的数据赋值,因为劫持的数据在vm的一级根下 //这里会促发监听器,从而改变 data 中的数据 vm[name] = event.target.value console.log(event.target.value, vm.data) }) // node.value = vm.data[name] node.removeAttribute('v-model') } new Watcher(vm, node, name, 'input'); } } } let regex = /\{\{(.*?)\}\}/ if (node.nodeType === 3) { //文本节点没有 attributes if (regex.test(node.nodeValue)) { const name = RegExp.$1.trim(); //text node.nodeValue = vm.data[name]; //将 data 的值赋给该 node,订阅,同上 //创建订阅者时,会调用监听器的 get 的方法,将此订阅者添加进订阅器中 new Watcher(vm, node, name, 'text'); } } } //编译 html 过程中,为每个 data 关联的节点生成一个 Watcher,收集依赖的时候会 addSub 到 subs 集合中,修改 data 数据的时候触发 dep 对象的 notify 通知所有 Wather 对象去修改对应视图 class Watcher { /** * @param {*} vm * @param {*} node * @param {*} name * @param {*} nodeType */ constructor(vm, node, name, nodeType) { Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.nodeType = nodeType; this.update(); // 更新视图 Dep.target = null;//设置为空,避免重复添加订阅者 } update() { //调用get, 这里 Dep.target不为空,getter 中会奖当前属性添加到订阅器集合中, update 函数执行完之后就不行了 this.get(); if (this.nodeType === 'text') { this.node.nodeValue = this.value; } if (this.nodeType === 'input') { this.node.value = this.value; } } get() { this.value = this.vm[this.name]; // 这里触发监听器的 get 的方法 } } // 定义一个订阅器 class Dep { constructor() { this.subs = []; } addSub(sub) { // 添加订阅者 this.subs.push(sub); } notify() { // 通知所有订阅者 this.subs.forEach(sub => { sub.update(); }) } } //定义一个Vue class Vue { constructor(opt) { this.el = document.querySelector(opt.el) this.data = opt.data console.log(this) observe(this.data, this) // 启动监听所有的 data 数据 this.el.appendChild(nodeToFragment(this.el, this)) } }
渲染
1创建代码片段,
2.1将节点遍历进代码片段
2.2同时遍历的节点进入代码解析器,
3判断节点类型
3.1标签类型 解析器遍历代码的属性
3.2 文本类型
3.3判断含有相对应指令或模板
3.4 如果有则对view视图进行赋值
响应数据绑定
4.1创建监听器,进行数据劫持和监听
3.4.1 接3.4 对当前节点进行事件的监听
4.2当数据改变时,改变vm一级下的数据,进行赋值,因为劫持的数据在vm的一级根下,这时触发监听器
4.3监听器,数据改变,触发set方法,给vm.data相对应数据赋值
实现数据双向绑定
当前背景,只是input改变改变model也就是data中的数据,实现了单向的绑定,如何通过model,data的数据改变来改变视图view呢?
采用 发布/订阅者模式
5 创建一个订阅器Dep
6 创建一个订阅者Watcher(可以接收信息,更新渲染自身和添加进订阅器),会自调用自身的方法update,update的方法会对view层进行赋值渲染
6.1 watcher中的update方法会触发get方法从而会触发监听器中的get方法,此时会往订阅器添加订阅者
3.2.1 判断节点类型,对相对应的节点进行赋值 所以3.4步骤在此废弃
当 observe 监听器中监听数据改变时,调用订阅器的 notify 方法遍历通知所有订阅者
每当创建一个Vue实例的时候,主要做了两件事情,第一是监听数据:observe(data),第二个是编译HTML:nodeToFragment(id)。
在监听数据过程中,为data的每一个属性生成主题对象dep。
在编译HTML的过程中,为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
前面已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的updata方法 => 更新视图,实现这个目标的关键是如何将watcher添加到关联属性的dep中去。
1. 定义主题对象Dep,对象中有addSub和notify两个方法,前者负责向当前对象中添加订阅者,后者轮询订阅者,调用订阅者的更新方法update()
2. 定义观察者对象方法Watcher,在方法中先将自己赋给一个全局变量Dep.target,其实是给主题类Dep定义了一个静态属性target,可以直接使用Dep.target访问这个静态属性。然后给类定义共有属性name(vue实例中的访问器属性名“text”),node(html标签,如<input>,{{text}}),vm(当前vue实例),nodeType(html标签类型),其次执行update方法,进而执行了原型对象上的get方法,get方法中的this.vm[this.name]读取了vm中的访问器属性,从而触发了访问器属性的get方法,get方法中将wathcer添加到对应访问器属性的dep中,同时将属性值赋给临时变量value。再者,获取属性的值(保存在临时变量value中),然后更新视图。最后将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
3. 在编译方法compile中,劫持子节点的时候,在节点上定义一个观察者对象Watcher
4. defineReactive方法中,定义访问器属性的时候,在存取描述符get中,如果主题对象类的静态属性target有值, 此时Watcher方法被调用,给主题对象添加订阅者。
data中的数据重新定义为访问器属性,get中将当前数据对应的节点添加到主题对象中,set方法中通知数据对应的节点更新。编译过程将data数据生成数据节点,并生成一个观察者来观察节点变化。
4. 总结
介绍了这么多,最后vue的原理总结如下。vue功能远不止这些
给Vue定义data选项;
使用Object.defineProperty将data选项中的属性转化成getter,setter属性;
getter将观察者添加到主题对象中,收集依赖;
setter中通知变更,给dom元素赋值;
编译,将el节点及子节点编译到render函数;
编译,触发getter,将当前节点对应的data属性添加到主题中;
编译,监听input节点的change事件,修改当前节点对应的data属性值,触发setter,通知变更,更新dom节点值,反馈给用户;
浙公网安备 33010602011771号