zoukankan      html  css  js  c++  java
  • Vue的响应式和双向绑定的实现

     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>
    

      

  • 相关阅读:
    DBCP数据库连接池
    Java Ant build.xml详解
    AWK 用法
    java打jar包
    linux 下java jar包的方法
    linux下java命令行引用jar包
    java webservice
    设计模式的几大原则
    ContextLoaderListener
    WebApplicationContextUtils源码
  • 原文地址:https://www.cnblogs.com/Helen-code/p/13536593.html
Copyright © 2011-2022 走看看