发布-订阅模式关于Vue数据双向绑定原理的应用

发布-订阅模式关于Vue数据双向绑定原理的应用

前言

本章主要讲诉发布-订阅模式关于Vue数据双向绑定原理的应用,全程大概耗时10分钟。

一、Vue和MVVM

Vue是一个MVVM模式的框架,即Model-View-ViewModel

  1. Model是数据
  2. View是视图
  3. ViewModel是View和Model之间的桥梁,负责监听View(Model)的变化,并通知Model(view)更新,如下图所示:

在这里插入图片描述
数据双向绑定,用兵法就说就是敌不动我不动,敌一动我跟着动。也就是说用户对View视图进行更新时,Model数据也会跟着修改;而我们修改Model数据时,View视图也一样会跟着修改。而其原理,就是利用了发布-订阅模式+数据劫持实现的。我们先来说一下什么是数据劫持?

二、数据劫持

数据劫持,顾名思义,就是在操作数据的时候,劫持这个操作顺便做一些我们想做的事情。
在Vue3.0之前,数据劫持是利用了Object.defineProperty来劫持对象属性的setter和getter操作来实现的。getter就是获取某个属性,setter就是设置某个属性。看下下面的代码实例:

var people = {
   name:'pjj'
 }
 Object.keys(people).forEach(function(key) {
   Object.defineProperty(people, key, {
     get:function(){
         console.log('获取属性时劫持触发这个console.log');
     },
     set:function(){
         console.log('设置属性时劫持触发这个console.log');
     }
   })
 })
 people.name; //控制台会打印出 “获取属性时劫持触发这个console.log”
 people.name = 'panjj'; //控制台会打印出 "设置属性时劫持触发这个console.log"

在Vue3.0,数据劫持改成了由Proxy来实现
使用Proxy代替Object.defindProprety的很大原因是因为原先的数据劫持,如果属性值为复杂类型的数据,则需要进行深度遍历,没办法直接监听。Proxy是直接监听整个对象的,简单很多,但是是ES6的语法,所以兼容性不是很好。

let people = {
	name: 'pjj'
}
let handler = {
	get: function(target, key) {
		console.log('获取属性时劫持触发这个console.log');
        return key in target ? target[key] : 'newKey';
    },
	set: function(target, key, newVal) {
		let res = Reflect.set(target, key, newVal);
		console.log('设置属性时劫持触发这个console.log');
		return target[key] = newVal;
	}
}
let p = new Proxy(obj, handler);
//target指的是要被Proxy包装的任意目标对象
//handler是一个对象,这个对象的属性是一些执行行为的函数
p.name = 'panjj';	//控制台会打印出 “获取属性时劫持触发这个console.log”
console.log(p.age); //控制台会打印出 “设置属性时劫持触发这个console.log”

从上面的两段代码,我们可以看出,其实数据劫持就是劫持了数据属性的get和set,让数据的属性在被获取get或者设置set时做出额外的操作。

基于这一点,只要我们把这个额外操作的用途用来监听数据和更新视图,其实就可以去实现数据->视图的更新了。
视图->数据的更新,其实可以利用一些监听事件去实现,例如像input事件就可以实现了。

三、数据双向绑定的实现

在这里插入图片描述
先进行一下梳理:

  1. 实现一个Observer,用来劫持,将属性转换成访问器属性getter和setter,用来数据监听。
  2. 实现一个Watcher,Watcher是数据的观察者,观察数据的更新,并执行相应的更新函数,从而更新视图。
  3. 实现一个Dep,用来连接Observer和Watcher。每一个observer会创建一个Dep实例,在get时让每一个watcher监听数据,在set时执行watcher里面的更新回调函数。
  4. 实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)。

Observer和Dep

//用来劫持并监听所有属性,如果有变动的,就通知订阅者。

function Observer(data) {
	this.data = data;
	this.walk(data); // 遍历data的每个属性,进行数据劫持
}

Observer.prototype = {
	walk: function(data) {
		var self = this;
		//这里是通过对一个对象进行遍历,对这个对象的所有属性都进行监听
		Object.keys(data).forEach(function(key) {
			self.defineReactive(data, key, data[key]);
		});
	},
	defineReactive: function(data, key, val) {
		//数据劫持
		var dep = new Dep();
		// 递归遍历所有子属性
		Object.defineProperty(data, key, {
			get: function getter() {
				if (Dep.target) {
					// 在这里添加一个订阅者,Dep.target????
					dep.addSub(Dep.target);
				}
				return val;
			},
			// setter,如果对一个对象属性值改变,就会触发setter中的dep.notify(),通知watcher(订阅者)数据变更,执行对应订阅者的更新函数,来更新视图。
			set: function setter(newVal) {
				if (newVal === val) {
					return;
				}
				val = newVal;
				// 新的值是object的话,进行监听
				childObj = observe(newVal);
				dep.notify();
			}
		});
	}
};

// 返回实例化的对象
function observe(value, vm) {
	if (!value || typeof value !== 'object') {
		return;
	}
	return new Observer(value);
}

// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数
function Dep() {
	this.subs = []; //存储订阅者的subs
}
Dep.prototype = {
	// 添加订阅
	addSub: function(sub) {
		this.subs.push(sub);
	},
	// 通知订阅者数据变更
	notify: function() {
		this.subs.forEach(function(sub) {
			sub.update();
		});
	}
};
Dep.target = null;

Watcher

//可以收到属性的变化通知并执行相应的函数,从而更新视图。

function Watcher(vm, exp, cb) {
	this.vm = vm; // vm 就是一个new Vue对象
	this.exp = exp; // exp就是v-model或者v-on等等绑定的属性,例如v-modle='name',exp就是name
	this.cb = cb; // cb就是Watcher绑定的更新函数
	this.value = this.get(); // 这里的value也就是下面的22行的value,是为了方便下面进行update时做新旧值的对比
}

Watcher.prototype = {
	update: function() {
		var value = this.vm.data[this.exp]; // 新value
		var oldVal = this.value; // 原value
		if (value !== oldVal) {
			// 不相等才进行更新
			this.value = value;
			this.cb.call(this.vm, value, oldVal); // call调用更新函数cb进行更新
		}
	},
	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; // 这里让Dep.terget指向了自己(一个watcher)
		var value = this.vm.data[this.exp]; // 这里this.vm.data[this.exp]也就是调用了上面例子中data的name,从而触发object.dedefineProperty中的get函数,把watcher添加到订阅器中
		Dep.target = null; // 释放自己
		return value;
	}
};

Compile

function Compile(el, vm) {
	this.vm = vm;
	this.el = document.querySelector(el); // 绑定的根元素
	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元素不存在');
		}
	},
	// 将绑定的dom元素整个添加到fragment元素
	nodeToFragment: function(el) {
		var fragment = document.createDocumentFragment();
		var child = el.firstChild;
		while (child) {
			// 将Dom元素移入fragment中
			fragment.appendChild(child);
			child = el.firstChild;
		}
		return fragment;
	},
	// 解析element
	compileElement: function(el) {
		var childNodes = el.childNodes;
		var self = this;
		[].slice.call(childNodes).forEach(function(node) {
			var reg = /\{\{(.*)\}\}/; // 通过正则来获取胡子语法{}里面的data
			var text = node.textContent;
			// 如果是元素节点
			if (self.isElementNode(node)) {
				self.compile(node);
				// 如果是文本节点
			} else if (self.isTextNode(node) && reg.test(text)) {
				//第 0 个元素是与正则表达式相匹配的文本 reg.exec(text)[0] 为 '{{data}}'
				//第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本 reg.exec(text)[1]为'data'
				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) {
		var self = this;
		var val = this.vm[exp];
		this.modelUpdater(node, val); // 完成挂载,{{ }}中的值被渲染为data中的值
		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;
	}
};

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值