欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue双向数据绑定之原理及实现3

程序员文章站 2024-02-01 14:25:22
...

一、实现原理

1. 从数据到视图的更新,是需要对数据进行监听劫持,这里我们设置一个监听器Observer来实现对所有数据的监听;

2. 设置一个订阅者Watcher,收到属性的变化通知并执行相应的函数,从而更新视图;

3. 设置一个解析器Compiler,解析视图DOM中所有节点的指令,并将模板中的数据进行初始化,然后初始化对应的订阅器。

二、Observer监听器

Observer是一个数据监听器,核心是Object.defineProperty(),对所有属性监听,利用递归来遍历所有的属性值,对其进行Object.defineProperty()操作。

// observer.js文件
function Observer(data) {    // 监听器
	this.walk(data)
}

Observer.prototype = {
	walk(data) {
		if( !data || typeof data !== 'object') {
			return false
		}

		Object.keys(data).forEach(key => { // 遍历操作
			this.defineProperty(data, key, data[key])
		})
	},
	defineProperty(data, key, val) { // set/get方法
		this.walk(val) // key 对应的又是 字典对象时
		
		Object.defineProperty(data, key, {
			enumerable: true,
			configurable: false,
			get() {
				return val
			},
			set(newVal) {
				if(val !== newVal) {
					console.log('新值: '+ newVal);
					val = newVal
				}
			}
		})
	}
}

// 测试数据
var zhangsan = {
	name: '张三',
	age: 18,
	score:{
		math: 100,
		english:90
	}
}

// 添加监听
new Observer(zhangsan)
zhangsan.age = 20
zhangsan.score.english = 99

确保都数据中的每个属性都转为getter/setter方法!

三、Dep订阅器

在最开始的结构中,通过Observer劫持并监听数据,当属性发生改变时,即通知调用到Watcher执行界面的更新。

这先建立一个简单的概念,Observer相当于作家,Watcher相当于读者(订阅者),现在是属于直接的关系。
但假如有新的Watcher订阅者时,怎么办呢?
Vue双向数据绑定之原理及实现3

这就还需要一个订阅器Dep,订阅器Dep是用来收集所有订阅者的。
Dep相当于是杂志社,Watcher作为订阅者,首先需要向杂志社订阅杂志,这样当有新的杂志(消息)产生时,Dep才会通知Watcher,如下图所示:
Vue双向数据绑定之原理及实现3

// dep.js文件
function Dep(){	// 订阅器
	this.subs = []
}

Dep.prototype = {
	addSub(watcher){		// 添加订阅者
		this.subs.push(watcher)
	},
	
	notify(){	// 通知订阅者
		this.subs.forEach(watcher=>{
			watcher.run()
		})
	}
}

四、Dep订阅器如何使用

Dep主要的功能是添加新的订阅者,以及通知订阅者。
但什么时候添加订阅者,什么是通知订阅者呢?

- 当数据发生改变时,即set的时就要通知订阅者; [新杂志发布(数据更新set),这就是通知订阅者]
- 在获取数据时,即get时添加订阅者; [想要阅读杂志,这要先获取到杂志(获取数据get)]

杂志社和订阅者的示例,进行对比才知道为什么放在set或get方法中!
在获取的时(get)就需要加上条件限制,并不是所有获取的都是 添加订阅者操作。

// Observer.prototype中的defineProperty
defineProperty(data, key, val) { // set/get方法
	this.walk(val) // key 对应的又是 字典对象时
	
	// 订阅器
        var dep = new Dep()
	
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: false,
		get() {
			if(条件){	// 符合某个条件说明是新的订阅者,才进行添加操作
				dep.addSub(watcher);
			}
			
			return val;
		},
		set(newVal) {
			if(val !== newVal) {
				console.log('新值: '+ newVal);
				val = newVal;
				
				// 通知订阅者
				dep.notify()
			}
		}
	})
}

五、Watcher订阅者

Watcher订阅者,在开始的时就要是添加到Dep订阅器中(杂志社当有新的杂志产生时,才知道通知谁。

其实也就是在Observer监听器的get方法中执行时,添加Watcher订阅者操作。但get方法会被调用多次,这就可以在Dep订阅器中添加一个Dep.target标识是否为新订阅者,添加成功后再将其去掉。
记住: 只有是新的订阅者,才是添加操作,否则不添加操作!

// watcher.js文件
// vm Vue的实例
// exp data中的key
// cb 回调函数
function Watcher(vm, exp, cb){
    this.vm = vm
    this.exp = exp
    this.cb = cb
    
    // 获取key对应的值,同时将watcher添加到Dep的队列中
    this.value = this.get() 
}

Watcher.prototype = {
	get(){	// 获取数据时,添加订阅者
		// 添加一个标识的意思
		Dep.target = this
		
		// 获取值,即触发get方法 [添加订阅者]
		var val = this.vm[this.exp]
		
		// 已经添加完成
		Dep.target = null
		
		return val
	},
	update(){	// 更新界面
		this.run()
	},
	run(){
		var val = this.vm[this.exp]
		
		if(val != this.value){	// 新值和旧值判断
			var oldValue = this.value
			this.value = val
			this.cb.call(this.vm, val, oldValue) // 回调函数
		}
	}
}

Observer监听器中,条件就有了,添加上即可:

// observer.js文件的 Observer.prototype
defineProperty(data, key, val) { 
	this.walk(val) // key 对应的又是 字典对象时
	
	// 订阅器
	var dep = new Dep()
	
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: false,
		get() {    // Dep.target存储的就是Watcher实例
			if(Dep.target){	// 判断是否要添加(新订阅者) 
				dep.addSub(Dep.target)
			}
			
			return val;
		},
		set(newVal) {
			if(val !== newVal) {
				console.log('新值: '+ newVal)
				val = newVal
				
				// 通知订阅者
				dep.notify()
			}
		}
	})
}

基本流程操作,参考下图: Vue双向数据绑定之原理及实现3

六、效果

从上图中可以看到,现阶段只是实现一个简单的从数据到视图的更新,后面我们再完善解析器Compiler(备注: 此时效果还是有问题的)。

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>

	<body>
		<div id="app">
			<h3>名字: {{name}}</h3>
			<h3>技能: {{skill}}</h3>
		</div>

		<div>
			请输入你的名字:<input type="text" class="text" value="" />
			<input type="button" class="bt" value="确定" />
		</div>

		<script src="dep.js" type="text/javascript" charset="utf-8"></script>
		<script src="watcher.js" type="text/javascript" charset="utf-8"></script>
		<script src="observer.js" type="text/javascript" charset="utf-8"></script>

		<script type="text/javascript">
			function Vue(options) {
				this.el = options.el // 元素
				this.data = options.data // 数据
				this.watcher = {} // 属性、数据、元素 的关联

				Object.keys(this.data).forEach(key => { // 数据代理
					this.proxyKeys(key);
				})

				// 监听器
				new Observer(this.data);

				// 解析DOM
				this.compile()
			}

			Vue.prototype = {
				proxyKeys(key) {
					var self = this
					Object.defineProperty(this, key, {
						enumerable: false,
						configurable: true,
						get() {
							return self.data[key]
						},
						set(newVal) {
							self.data[key] = newVal
						}
					})
				},

				compile() { // 解析DOM即更新数据
					// 获取到对象绑定的元素
					var ele = document.querySelector(this.el);

					// 所有子元素
					var childEls = ele.childNodes;

					// 创建fragment
					var fragment = document.createDocumentFragment();

					// 获取到第一个子元素
					var child = ele.firstChild;
					while(child) {
						// 将Dom元素移入fragment中
						// appendChild: 如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除,然后再插入到新的位置
						fragment.appendChild(child)
						// 再获取第一个(其实就是下一个下一个的操作)
						child = ele.firstChild
					}

					// 遍历所有子元素
					[].slice.call(fragment.childNodes).forEach(el => {
						var reg = /\{\{(.*)\}\}/;
						var text = el.textContent;
						if(reg.test(text)) {
							var key = reg.exec(text)[1];
							el.textContent = this.data[key];

							// 新的订阅者
							new Watcher(this, key, value=>{
        							ele.innerHTML = value;
    							})
						}
					})

					// 添加
					ele.appendChild(fragment)
				}
			}

			// 创建Vue对象
			var myVue = new Vue({
				el: '#app',
				data: {
					name: '阿童木',
					skill: 'web前端开发',
				}
			})

			// 修改名字
			document.querySelector('.bt').onclick = function() {
				myVue.data.name = document.querySelector('.text').value
			}
		</script>
	</body>

</html>
相关标签: Vue vue