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>

  

原文地址:https://www.cnblogs.com/Helen-code/p/13536593.html