「Vue系列」之Vue2的数据劫持、发布订阅者模式、diff算法

       最近一段时间自己找点东西学习,因为面试vue双向绑定问的挺多的,就想去深入研究一下本质原理,说是本质也不算,就是看了看别人的研究总结一点自己的看法和理解。

数据劫持
       首先想实现双向绑定,肯定要先知道数据的变化,这时候应该怎么做?就是使用一个方法进行数据劫持。这个方法就是Object.defineProperty()。

介绍一下这个方法:
       Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。详细解释链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
下面就是写一个简单的数据监听器:

/* 实现数据监听器(数据劫持)*/
	var obj = {
		name: '我是一本书',
		keys: {
			name:'我是一棵树'
		}
	}
	function kidnapFun(obj){
		if (!obj || typeof obj !== 'object') {
			return
		}
		Object.keys(obj).forEach((key) => {
			addKidnap(obj, key, obj[key])
		})
	}
	function addKidnap(obj, key, val) {
		kidnapFun(val) // 递归所有子属性
		Object.defineProperty(obj, key, {
			configurable: true, // 为 true 时,该属性描述符才能够被改变或者删除
			enumerable: true, // 为true时,该属性才能够出现在对象的枚举属性中
			get: function () { // 访问属性时,该方法会被执行,方法执行时没有参数传入
				return value;
			},
			set: function (value) { // 当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值
				return console.log('我已经被修改为' + value)
			}
		})
	}
	kidnapFun(obj)
	obj.name = '我现在不是一棵树了'

       上面就是一个简单的数据监听器了,其实写到这里我觉得已经够了呀,还要什么发布订阅者模式干啥? 还要diff算法干啥?

看一下自己这个和vue的区别,嗯,是我目光短浅了。

下面就要说到发布订阅者模式
注意是 发布订阅者模式 而不是 观察者模式

       这两个是有区别的,具体区别是什么,参见下面这篇文章:
https://www.cnblogs.com/viaiu/p/9939301.html

在了解发布订阅者模式之后我们再回到这里
       既然是发布订阅者,那么我们首先建造一个容器来储存我们的订阅者

这个容器被我们称作 依赖收集器

// 创建容纳观察者的依赖收集器(发布订阅者模式开始) 订阅器提供两个方法 1.添加订阅者 2.通知订阅者
function Dep(){
	this.subs = [] // 容器
}
// 将方法定义到构造方法的prototype上,这样的好处是通过该构造函数生成的实例所拥有的方法都是指向一个函数的索引,这样可以节省内存
Dep.prototype = {
	addSub: function (sub) { // 添加订阅者方法
		this.subs.push(sub) 
	},
	notify: function () { // 通知订阅者
		this.subs.forEach((sub)=>{
			// 调用观察者的更新方法
		})
	}
}

有了容器,那么我们就来建造我们的 观察者
重点:
vue的更新dome的diff算法位置在观察者更新自己的方法里面

有关diff算法的解释参见这篇文章(看完记得回来?)
https://www.cnblogs.com/wind-lanyan/p/9061684.html

// 创建创建观察者 观察者提供2个方法 1.更新自己 2.将自己添加到订阅器里面
// vue的更新dome的diff算法位置在观察者更新自己的方法里面
function Watcher() {
	
}
Watcher.prototype = {
	update: function () { // 更新自己的方法
		(diff算法位置)
	},
	get: function () { // 将自己添加到订阅器
		
	}
}

以上就是简单的发布订阅者模式的每个部分的组成了。
下一步那就是我们给这几个部分组装一下:
(代码有点多,请结合上面注释使用哦?)
数据监听器 订阅器 订阅者 初步综合

function observe(data) {
		if (!data || typeof data !== 'object') {
			return
		}
		Object.keys(data).forEach((key)=>{
			defineReactive(data, key, data[key])
		})
	}
	function defineReactive(data, key, value) {
		observe(value);
		var dep = new Dep(); // 创建订阅器(????)
		Object.defineProperty(data, key, {
			get: function () {
				if (Dep.target) { // 如果订阅者存在,则添加到订阅器
					dep.addSub(Dep.target)
				}
				return value
			},
			set: function (newValue) {
				if (value === newValue) {
					return
				}
				value = newValue
				dep.notify()
			}
		})
	}
	// 订阅器
	function Dep() {
		this.subs = []
	}
	Dep.target = null // 中间介
	Dep.prototype = {
		addSub: function (sub) { // 添加订阅者
			this.subs.push(sub)
		},
		notify: function () {
			this.subs.forEach(function(sub){ // 通知所有的订阅者
				sub.update()
			})
		}
	}
	// 观察者
	function Watcher(vm, keys, fun) { // vm:this keys:key fun: 更新函数
		this.vm = vm
		this.keys = keys
		this.fun = fun
		this.value = this.get()
	}
	Watcher.prototype = {
		update: function () { // 更新
			let value = this.vm.data[this.keys]
			let oldValue = this.value
			if (value !== oldValue) {
				this.value = value
				this.fun.call(this.vm, this.value)
			}
		},
		get: function () { // 添加
			Dep.target = this; // 1.先通过中间介缓存自己
			let value = this.vm.data[this.keys]// 2.调用已经添加监听器的属性的get方法储存自己
			Dep.target = null; // 3.最后释放中间介
		}
	}
	// 连接器
	function selfVue(data, el, keys) {
		this.data = data
		observe(data)
		el.innerHTML = this.data[keys]
		new Watcher(this, keys, function(value){
			el.innerHTML = value
		})
	}
	var el = document.querySelector('#one') // querySelector 获取dome元素
	var selfVue = new selfVue({name: '我一会就变成一棵树了'}, el, 'name');
	
	window.setTimeout(()=>{
		selfVue.data.name = '我现在变成一棵树了'
	}, 3000)

以上就完成了一个简单的组装了,休息一会哈
顺便请教各位看官一个问题:
(也不知道有没有人看?)

//就是我上面打问号的地方,每一个属性都创建一个依赖收集器,作用是什么? 不是很重复吗?
//不知道vue源码是怎么写的,或者是我这边有点问题,希望各位大佬指导一下
解答在下面的评论,感谢一波大佬!!

再接着就是实现一个指令解析器
我就直接贴完整的代码,包括优化指令
(代码有点多,这都是我看着人家的文章一行一行敲的,如有雷同,与我无关?)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>最后的文件</title>
	</head>
	<body>
		<div id="app">
			<h2>{{title}}</h2>
	        <input v-model="name">
	        <h1>{{name}}</h1>
	        <button v-on:click="clickMe">click me!</button>
		</div>
	</body>
	<script type="text/javascript">
		// observe compile watcher 关联器 入口
		
		//  递归函数
		function observe(data) {
		//	递归所有子属性
			if (!data || typeof data !== 'object') {
				return
			}
			Object.keys(data).forEach(function(key) {
				defineReactive(data, key, data[key]);
			})
		}
		//  数据监听器
		function defineReactive(data, key, val) {
			observe(val); // 调用递归函数
			var dep = new Dep(); // 创建订阅器
			Object.defineProperty(data, key, {
				enumerable: true,
				configurable: true,
				get: function() { // 查看键值的时候调用
					if (Dep.target) { // 如果观察者存在,则添加到订阅器里面
						dep.addSub(Dep.target)
					}
					return val;
				},
				set: function(newVal) { // 修改键值的时候调用
					if (val === newVal) {
						return
					}
					val = newVal;
					console.log('属性' + key + '已经被监听了,现在值为:' + newVal.toString() + ';')
					dep.notify() // 如果数据变化,通知所有已经添加的订阅者
				}
			})
		}
		// 订阅器
		function Dep() {
			this.subs = [] // 订阅者容器
		}
		Dep.target = null
		Dep.prototype = {
			addSub: function(sub) {
				this.subs.push(sub) // 添加订阅者
			},
			notify: function() {
				this.subs.forEach(function(sub) {
					sub.update() // 通知所有的订阅者
				})
			}
		}
		// 订阅者
		function Watcher(vm, exp, cb) {  // vm实际dom exp key名 cb更新函数
			this.cb = cb;
			this.vm = vm;
			this.exp = exp;
			this.value = this.get(); // 调用函数把自己存到订阅器中
		}
		Watcher.prototype = {
			update: function() {
				this.run()
			},
			run:function() {
				var value = this.vm.data[this.exp];
				var oldVal = this.value;
				if (value !== oldVal) {
					this.value = value;
					this.cb.call(this.vm, value); // 替换
				}
			},
			get:function() {
				Dep.target = this; //缓存自己
				var value = this.vm.data[this.exp]; // 调用监听器里面的get函数  将自己添加到订阅器里面
				Dep.target = null; // 释放自己
				return value;
			}
		}
		// 解析器
		function Compile(el, vm) {
		    this.vm = vm;
		    this.el = document.querySelector(el); // 获取dome元素
		    this.fragment = null;
		    this.init();
		}
		Compile.prototype = {
		    init: function () {
		        if (this.el) {
		            this.fragment = this.nodeToFragment(this.el); // 将该节点放置在创建的文档碎片中介中
		            this.compileElement(this.fragment);
		            this.el.appendChild(this.fragment);
		        } else {
		            console.log('Dom元素不存在');
		        }
		    },
		    nodeToFragment: function (el) {
		        var fragment = document.createDocumentFragment(); // 创建文档碎片
		        var child = el.firstChild;
		        while (child) {
		            // 将Dom元素移入fragment中
		            // a.appendChild(b) 将b的元素移除,然后添加到a中(这样懂了吧)
		            fragment.appendChild(child); 
		            child = el.firstChild
		        }
		        return fragment; // 返回文档碎片
		    },
		    compileElement: function (el) {
		        var childNodes = el.childNodes;
		        var self = this;
		        [].slice.call(childNodes).forEach(function(node) {
		            var reg = /\{\{(.*)\}\}/;
		            var text = node.textContent;
		            if (self.isElementNode(node)) {  
		                self.compile(node);
		            } else if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令
		                self.compileText(node, reg.exec(text)[1]);
		            }
		
		            if (node.childNodes && node.childNodes.length) {
		                self.compileElement(node);// 继续递归遍历子节点
		            }
		        });
		    },
		    compile: function(node) {
		        var nodeAttrs = node.attributes;
		        var self = this;
		        Array.prototype.forEach.call(nodeAttrs, function(attr) {
		            var attrName = attr.name;
		            if (self.isDirective(attrName)) {
		                var exp = attr.value;
		                var dir = attrName.substring(2);
		                if (self.isEventDirective(dir)) {  // 事件指令
		                    self.compileEvent(node, self.vm, exp, dir);
		                } else {  // v-model 指令
		                    self.compileModel(node, self.vm, exp, dir);
		                }
		                node.removeAttribute(attrName);
		            }
		        });
		    },
		    compileText: function(node, exp) {
		        var self = this;
		        var initText = this.vm[exp];
		        this.updateText(node, initText);  // 将初始化的数据初始化到视图中
		        new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
		            self.updateText(node, value);
		        });
		    },
		    compileEvent: function (node, vm, exp, dir) {
		        var eventType = dir.split(':')[1];
		        var cb = vm.methods && vm.methods[exp];
		
		        if (eventType && cb) {
		            node.addEventListener(eventType, cb.bind(vm), false);
		        }
		    },
		    compileModel: function (node, vm, exp, dir) {
		        var self = this;
		        var val = this.vm[exp];
		        this.modelUpdater(node, val);
		        new Watcher(this.vm, exp, function (value) {
		            self.modelUpdater(node, value);
		        });
		
		        node.addEventListener('input', function(e) {
		            var newValue = e.target.value;
		            if (val === newValue) {
		                return;
		            }
		            self.vm[exp] = newValue;
		            val = newValue;
		        });
		    },
		    updateText: function (node, value) {
		        node.textContent = typeof value == 'undefined' ? '' : value;
		    },
		    modelUpdater: function(node, value) {
		        node.value = typeof value == 'undefined' ? '' : value;
		    },
		    isDirective: function(attr) {
		        return attr.indexOf('v-') == 0;
		    },
		    isEventDirective: function(dir) {
		        return dir.indexOf('on:') === 0;
		    },
		    isElementNode: function (node) {
		        return node.nodeType == 1;
		    },
		    isTextNode: function(node) {
		        return node.nodeType == 3;
		    }
		}
		// 关联模块
		function SelfVue (options) {
			var self = this;
			this.data = options.data;
			this.methods = options.methods;
			Object.keys(this.data).forEach(function(key) {
				self.proxyKeys(key);  // 绑定代理属性
			})
			observe(this.data);
			new Compile(options.el, this);
			options.mounted.call(this);
		}
		SelfVue.prototype = {
			proxyKeys: function (key) {
				var self = this;
				Object.defineProperty(this, key, {
		            enumerable: false,
		            configurable: true,
		            get: function () {
		                return self.data[key];
		            },
		            set: function (newVal) {
		                self.data[key] = newVal;
		            }
		        });
			}
		}
		new SelfVue({
	        el: '#app',
	        data: {
	            title: 'hello world',
	            name: 'canfoo'
	        },
	        methods: {
	            clickMe: function () {
	                this.title = 'hello world';
	            }
	        },
	        mounted: function () {
	            window.setTimeout(() => {
	                this.title = '你好';
	            }, 1000);
	        }
	    });
	</script>
</html>

       好的,以上就是本文的全部内容了,这个只是初步的,都是我看网上各位大佬的文章写的自己的理解,虽然全是代码吧,但是注释很多,等之后再研究一下vue源码),再给大家补充吧。

欢迎关注公众号:廿九前端营地
可以获取前端资源、大厂外企内推或者联系我一起学习吹水

Tipes:往期精选
「Vue系列」之面试官问NextTick是想考察什么?
「Vue系列」之为什么用Proxy取代Object.defineProperty?
「Vue系列」之Vue3生命周期和新增setup的一些总结
「Vue系列」之Vue3初尝试Cannot find module ‘worker_threads‘报错
「Vue系列」之重新探索v-if和v-show
「Vue系列」之Vue2的数据劫持、发布订阅者模式、diff算法
「Vue系列」之Vue2实现当前组件重新加载

Logo

前往低代码交流专区

更多推荐