Vue的响应式和双向绑定的实现
这部分内容学习的是的是B站王红元老师的最后三节课https://www.bilibili.com/video/BV15741177Eh?p=229
- 在老师代码的基础上新增了对input这个元素节点的Watcher,这样就能够实现修改数据可以映射到input的value值,和官方Vue效果一样
- 新增了一些注释
如果有问题还请大佬批评指正
实现效果:
1.刷新后输入框中是data中的数据,当在输入框输入内容后,会改变data中的属性值并映射到页面
2.直接通过按钮修改data的数据,会同步修改页面上的值和input的value值
(不会附动画,只能贴图了。。。)
主体思路
前提概念
这部分代码要用到三个知识:大家查一下就懂了
1.Object.defineProperty()方法
2.Object.keys()方法
3.document.createDocumentFragment() 文档片段的使用
完整代码:可直接复制查看效果
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <input type="text" v-model="msg"> <br> {{msg}} <br> <button id="btn">按钮</button> </div> <!-- <script src='./vue-2.4.0.js'></script> --> <script> // 一:定义Vue构造函数 class Vue { constructor(options) { // 1.将传入的对象保存起来 this.$options = options this.$data = options.data this.$el = options.el // 2.将data添加到响应式系统中 // new Observer(this.$data) new Observer(this.$data) // 3.代理this.$data的数据,将其代理到Vue实例对象上 Object.keys(this.$data).forEach(key => { this._proxy(key) }) // 4.解析el中的{{}}模板标签 new Compiler(this.$el, this) // 传入#app元素和当前Vue的实例对象 } _proxy(key) { // 这里的主要作用就是将this.$data中的变量直接代理到this上 /* 例如 const app = new Vue({ data: { msg: '举例' } }) 此时,传入new Vue({})中的对象的data,会被保存为app.$data中,使用方法app.$data.msg 代理的作用就是能够直接通过app.msg来访问这个变量,原Vue也有同样的代理设置,效果也是这样 */ Object.defineProperty(this, key, { // this是当前的Vue实例,直接将this.$data中的属性名key设为vue实例的属性 enumerable: true, configurable: true, get() { return this.$data[key] // 当访问app.msg的时候,返回的是app.$data.msg的值,并且这一步会触发Observer中设置的对app.$data的get代理 }, set(newValue) { this.$data[key] = newValue // 当给app.msg = "新的值",是给app.$data.msg = "新的值",并且这一步也会触发Observer中设置的对app.$data的set代理 } }) } } // 二:定义Observer构造函数,监听对象数据的改变 class Observer { constructor(data) { this.data = data // console.log(data) // console.log(this.data) Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, value) { // 每一个属性都对应一个dep订阅器对象,用来存放使用这个属性的所有订阅者 const dep = new Dep() Object.defineProperty(data, key, { enmuerable: true, // 属性可枚举 configurable: true, // 属性可删除 get() { // 用Watcher中定义的Dep.target全局属性来判断是否有新的watcher需要添加 if (Dep.target) { dep.addSub(Dep.target) // 将Dep.target中保存的watcher添加到dep中 } return value }, set(newValue) { if (value === newValue) return value = newValue // 当给属性赋新的值时候,触发dep.notify方法 dep.notify() } }) } } // 三:定义Dep订阅器构造函数,用来存放所有的订阅者watcher class Dep { constructor() { this.subs = [] } addSub(sub) { this.subs.push(sub) } notify() { // 遍历dep中所有的watcher,调用他们的update函数,去获取新的属性值 this.subs.forEach(item => { item.update() }) } } // 四:定义Watcher订阅者构造函数 class Watcher { constructor(node, name, vm) { this.node = node // 通过正则匹配的{{}}这种文字节点 this.name = name // {{}}节点的内容,也就是{{变量名}}其中的变量名 this.vm = vm //当前Vue的实例对象 // 定义一个Dep.target全局属性用来存放要加入订阅器的watcher,当添加完成后就清空 Dep.target = this //这里新增一个全局属性Dep.target,保存了当前的watcher this.update() Dep.target = null } // 更新页面数据 update() { // 1.把页面上{{变量名}}用vm.data.变量名 替代 // 2.update方法在两种情况下被调用 // 2.1 new一个Watcher对象的时候调用this.vm.data[this.name]读取了data中的属性值,触发了defineReactive中的get方法,于是将Dep.target保存的当前的watcher添加到dep中去 // 2.2 data属性被赋新值的时候,触发notify()=>update(),将新的值重新赋给页面上的节点,但此时Dep.target为空,所以不会添加新的watcher到dep // console.log(this.vm.$data[this.name]) // this.node.nodeValue = this.vm[this.name] if (this.node.nodeName === 'INPUT') { // 因为input标签的node节点和赋值和{{}}这种文本节点不一样,所以这里加个判断 this.node.value = this.vm[this.name] } this.node.nodeValue = this.vm[this.name] } } // 五: 定义Compiler,用来解析模板指令{{}},将模板中的变量替换成数据 const reg = /{{(.+)}}/ //正则表达式,用来匹配{{}}模板 class Compiler { constructor(el, vm) { console.log(el) // #app this.el = document.querySelector(el) // 这里el中保存的是#app,可以直接选中页面上的#app元素 console.log(this.el) this.vm = vm // 当前Vue的实例保存在this.vm中 this.frag = this._createFragment() // 此时frag中保存的是从#app取出来的所有子节点,再将他们添加会#app中 this.el.appendChild(this.frag) } _createFragment() { // 创建一个新的空白的文档片段( DocumentFragment),这个文档片段一般用来添加元素,然后将整个空白文档添加到DOM上去,空白文档本身不会显示 // 因为空白文档是存在于内存中的,所以频繁的添加元素不会影响到DOM重绘和回流,等元素添加完毕将空白文档挂载到DOM上就好了 const frag = document.createDocumentFragment() let child while (child = this.el.firstChild) { // 将#app的子节点一个个拿出来 // console.log(child) // 这里有个问题页面上{{msg}}对应的文本节点nodeValue显示是"",不是{{msg}} // 将节点一个个从#app中取出来去判断 this._compile(child) // 将取出来的节点添加到frag中,但这样取出的节点在#app中就不会存在了,之后需要将frag再添加回#app中去 frag.appendChild(child) } return frag } _compile(node) { //判断节点类型 if (node.nodeType === 1) { //node.attributes是当前元素节点所有属性值的集合,是一个对象,可以通过node.attributes[0]使用第一个属性 // 也能通过node.attributes[属性节点名]获得属性节点 let attrs = node.attributes if (attrs.hasOwnProperty('v-model')) { // 判断这个节点对象有v-model这个属性 const name = attrs['v-model'].nodeValue // 获得v-model这个属性节点的value值,也就是msg // console.log(name) // msg // 匹配到这个属性后,就能新增input这个元素节点的Watcher(订阅者) new Watcher(node, name, this.vm) node.addEventListener('input', e => { // 监听当前输入框的input事件 this.vm[name] = e.target.value // 触发时将输入的数据赋给对应的属性 }) } } if (node.nodeType === 3) { //文本节点 // console.log(node) if (reg.test(node.nodeValue)) { console.log(RegExp.$1) const name = RegExp.$1.trim() // 取得正则匹配到的内容,去除两边的空格,其实匹配到就是页面上{{}}里面的变量名 // 匹配成功后,就能新增{{}}这个文本节点的Watcher(订阅者) new Watcher(node, name, this.vm) } } } } </script> <script> const app = new Vue({ el: '#app', data: { msg: '双向绑定' } }) document.getElementById('btn').addEventListener('click', () => { app.msg = '按下手动更改数据' }) </script> </body> </html>