精华内容
下载资源
问答
  • 看代码 var inp = document.querySelector('input');...//发布订阅对象 var obj = {}; //发布订阅事件列表 obj.list = {}; //定义订阅事件函数 obj.listen = function(eventName,fn){ obj.list[event...

    看代码

    var inp = document.querySelector('input');
    var p = document.querySelector('p');
    
    //发布订阅对象
    var obj = {};
    //发布订阅事件列表
    obj.list = {};
    //定义订阅事件函数
    obj.listen = function(eventName,fn){
      obj.list[eventName] = fn;
    }
    //定义发布事件函数
    obj.trigger = function(eventName){
      obj.list[eventName]();
    }
    
    //数据
    var data = {str:'hello'}
    var str = data.str;
    //观察数据变动从而触发所订阅事件
    Object.defineProperty(data, 'str', {
      get(){
        return str;
      },
      set(newVal){
        str = newVal;
        console.log(data.str);
        obj.trigger('go')
      }
    })
    //订阅一个事件
    obj.listen('go',function(){
      p.innerText = data.str;
      inp.value = data.str;
    })
    
    //数据变动
    inp.oninput = function(){
      data.str = this.value;
    }
    
    setInterval(function(){
      data.str+='a';
    },1000)

     

    展开全文
  • 发布-订阅模式关于Vue数据双向绑定原理的应用前言一、Vue和MVVM二、数据劫持三、数据双向绑定实现 前言 本章主要讲诉发布-订阅模式关于Vue数据双向绑定原理的应用,全程大概耗时10分钟。 一、Vue和MVVM 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;
    	}
    };
    
    
    展开全文
  • Vue双向绑定(数据劫持,发布订阅模式,diff算法) 最近一段时间自己找点东西学习,因为面试vue双向绑定问的挺多的,就想去深入研究一下本质原理,说是本质也不算,就是看了看别人的研究总结一点自己的看法和理解...

    Vue2.0双向绑定(数据劫持,发布订阅者模式,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源码(我不想研究?),再给大家补充吧。
    谢谢观看???

    展开全文
  • Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式实现双向数据绑定的。当把一个普通 Js 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,通过用Object.defineProperty ...

    首先看下vue官方文档中对于响应式原理说明的配图:

    Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式来实现双向数据绑定的。当把一个普通 Js 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,通过用Object.defineProperty 设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,通过setter在数据变更的时候通知订阅者更新视图。

    发布—订阅模式

    最近又看了一遍《javaScript设计模式与开发实践》中的发布订阅模式,正好结合Vue源码做个总结。

    概念:发布订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

    优点

    1. 时间上的解耦:发布-订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。

     2. 对象之间的解耦:发布-订阅模式让两个对象松耦合地联系在一起,相互通信不再需要显示的调用另外一个对象的接口。

    简单的发布-订阅模式的实现

    1. 首先要指定好谁充当发布者;

    2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者;

    3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数;

    发布订阅模式的通用实现

    var event = {
      clientList: [],
    
      listen: function (key, fn) {
        // 初始化特定key属性的缓存队列
        if (!this.clientList[key]) {
          this.clientList[key] = []
        }
        // 订阅的消息添加进缓存列表
        this.clientList[key].push(fn)
      },
    
      trigger: function () {
        var key = Array.prototype.shift.call(arguments)
        var fns = this.clientList[key]
    
        // 如果没有绑定对应的消息
        if (!fns || fns.length === 0) {
          return false
        }
    
        for (var i = 0, fn; fn = fns[i++]; ){
          fn.apply(this, arguments) // arguments 是 trigger 时带上的参数
        }
      },
    
      remove: function (key, fn) {
        var fns = this.clientList[key]
    
        // 如果key对应的消息没有被人订阅,则直接返回
        if (!fns) {
          return false
        }
    
        // 如果没有传入具体的回调函数,则表示需要取消key对应消息的所有订阅
        if (!fn) {
          fns && (fns.length = 0)
        } else {
          // 反向遍历订阅的回调函数列表
          for (var l = fns.length - 1; l >= 0; l--) {
            var _fn = fns[l]
            if (_fn === fn) { // 删除订阅者的回调函数
              fns.splice(l, 1)
            }
          }
        }
      }
    }
    
    // installEvent函数:给所有的对象都动态添加发布—订阅功能
    var installEvent = function( obj ){
      for ( var i in event ){
        obj[ i ] = event[ i ]
      }
    }
    
    // 测试:
    var salesOffices = {}
    
    installEvent(salesOffices)
    
    salesOffices.listen('squereMeter88', fn1 = function(price) {
      console.log('价格=' + price)
    })
    
    salesOffices.listen('squereMeter100', fn2 = function(price) {
      console.log('价格=' + price)
    })
    
    salesOffices.remove('squereMeter88', fn1)
    salesOffices.trigger('squereMeter100', 200)
    

    全局的发布-订阅对象的实现:

    订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会发送给哪些订阅者,Event 作为一个类似“中介者”的角色,把订阅者和发布者联系起来。

    var Event = (function(){
      var clientList = {},
          listen,
          trigger,
          remove;
    
      listen = function( key, fn ){
        if ( !clientList[ key ] ){
          clientList[ key ] = [];
        }
        clientList[ key ].push( fn );
      };
    
      trigger = function(){
        var key = Array.prototype.shift.call( arguments ),
            fns = clientList[ key ];
        if ( !fns || fns.length === 0 ){
          return false;
        }
        for( var i = 0, fn; fn = fns[ i++ ]; ){
          fn.apply( this, arguments );
        }
      };
    
      remove = function( key, fn ){
        var fns = clientList[ key ];
        if ( !fns ){
          return false;
        }
        if ( !fn ){
          fns && ( fns.length = 0 );
        } else {
          for ( var l = fns.length - 1; l >=0; l-- ){
            var _fn = fns[ l ];
            if(_fn === fn){
              fns.splice( l, 1 );
            }
          }
        }
      };
    
      return {
        listen: listen,
        trigger: trigger,
        remove: remove
      }
    
    })();

    发布订阅模式实现EventEmitter:

    通过 on 方法注册事件,trigger 方法触发事件,once 方法注册只触发一次的事件, off 方法注销事件。

    class EventEmitter {
      constructor() {
        this.subs = {}
      }
    
      on(event, cb) {
        (this.subs[event] || this.subs[event] = []).push(cb)
      }
    
      trigger(event, ...args) {
        this.subs[event] && this.subs[event].forEach(cb => {
          cb(...args)
        })
      }
    
      once(event, cbOnce) {
        let cb = (...args) => {
          cbOnce(...args)
          this.off(event, cbOnce)
        }
        this.on(event, cb)
      }
    
      off(event, cbOff) {
        if (this.subs[event]) {
          let index = this.subs[event].findIndex(cb => cb === cbOff)
          this.subs[event].splice(index, 1)
          if (!this.subs[event].length) delete this.subs[event]
        }
      }
    }

     

    展开全文
  • 主文件2.vue.js(模拟vue实例,进行数据代理)3.observer.js(数据劫持)4.eventEmitter.js(发布订阅操作)5.compiler.js (编译模板) 1.主文件 <!DOCTYPE html> <html lang="en"> <head> <meta...
  • Vue 双向绑定的原理及实现Demo

    千次阅读 2017-03-01 23:41:39
    Vue双向绑定的原理Vue用了一段时间了,一直没有纠结过它的原理,今天看了一篇很不错的文章:Vue.js双向绑定实现原理,跟着敲了一遍,发现其中有意思的地方还是很多的,一些知识我之前都没有接触过,这里要好好整理...
  • Vue.js是通过数据劫持以及结合发布者-订阅者来实现双向绑定的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来...
  • 代码简单原理的实现: &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;meta http-equiv="X-UA-Compatible" ...
  • i++) { defineReactive(val, keys[i], val[keys[i]]) // 这里的val[keys[i] 是key中key 是为了判定key还有没有其他的对象 } } //订阅者watcher 也就是依赖 export class Watcher{ constructor(vm, expOrFn, cb) { ...
  • vue.js 是采用数据劫持结合发布者-订阅模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由Object....
  • 之前也了解过vue是通过数据劫持+订阅发布模式实现MVVM的双向绑定的,但一直没仔细研究,这次深入学习了一下,借此机会分享给大家。 首先先将流程图给大家看一下 参考文章:Vue.js双向绑定实现原理 我虽然参考...
  • 数据劫持,订阅模式双向绑定 //index.html文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial...
  • 前段时间笔者学习了一下Vue的源码,也看了一些前辈对Vue源码研究的博客,然后用es实现了一个基础的数据双向绑定框架Hue,作为学习成果之一,在此分享给大家。Hue实现了@click,v-model, watch监听属性变化这几个...
  • 文章目录前言参考资料初级版本实现publisher实现消息订阅中心实现Subscriber实现绑定函数完整代码进阶引入compile,并封装成MVVMES6 class语法版本结语 前言 单向绑定非常简单,就是把Model绑定到View,当我们用...
  • Vue实现数据双向绑定

    2020-08-15 21:18:33
    1、Vue实现数据双向绑定的原理 Object.defineProperty() 2、vue实现数据双向绑定主要步骤: 2.1 实现Observer 对于需要观察数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter,这样的话,给这...
  • 每当去找工作面试的时候,当面试官问到vue的双向数据绑定或者响应式原理的时候,面试者总是不加思索的张口就来:采用发布者-订阅模式。。。。。一顿神侃。真不知道是梁静茹给了你们多少勇气说出的这句话。这么说...
  • 几种实现双向绑定的做法 目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加...发布者-订阅模式(backbone.js) 脏值...
  • vue的双向绑定指的是数据变化更新视图,视图变化更新数据。 视图变化更新数据一般就采用事件监听的方式就可以了,数据变化更新视图就需要涉及响应式原理。 vue2.x的响应式原理的基础是Object.defineProperty属性。...
  • 通过 Proxy 实现数据双向绑定前言defineProperty 缺陷defineProperty 双向绑定Proxy 语法Proxy 双向绑定 前言 用过 Vue 的人都知道 Vue 有双向绑定的功能,Vue 2 是通过 Object.defineProperty 实现双向绑定,但是...
  • 3.发布订阅模式 4.vm. $ data和&vm。$ methods挂载到vm上 2.运行 git clone https://github.com/evanchen0629/vue-core.git cd vue-core npm install 或 yarn(推荐) npm run dev 或 yarn dev
  • ... MVVM拆开来即为Model-View-ViewModel,有View,ViewModel,Model三部分组成。View层代表的是视图、模版,负责将数据模型转化为UI展现出来。Model层代表的是模型、数据,可以在Model层中定义数据修改和操作的...
  • 发布订阅模式代码实现

    千次阅读 2020-04-26 23:14:12
    发布订阅模式简介:订阅者(Subsciber)通过事件注册(Subscribe)将订阅事件提交到调度中心(Topic),调度中心保存好订阅者的注册信息(回调函数),每当发布者(Publisher)发布事件的时候,通过事件发布(Publish...
  • 一、实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向数据绑定的做法大致有如下三种: 1.发布者-订阅模式(backbone.js) 思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的...
  • 前言 技术从来不会受限于语言,受限你的,永远只是思想。... ...为例来进行编码,当然,我们需要事先了解一下 ...就像一个代理器,当有人对目标对象进行处理(set、has、get 等等...双向绑定的完整例子已放在 codepen 上
  • 主要介绍了Vue双向绑定实现原理与方法,结合实例形式详细分析了发布者-订阅模式、脏值检查、数据劫持与双向绑定相关实现技巧,需要的朋友可以参考下
  • 1、如何实现双向绑定? 以用户提交表单为例,其原理是我们对input进行value的属性绑定(v-bind:value="…"),将Model中的变量绑定到...vue实现数据双向绑定主要是采用数据劫持,配合发布-订阅模式的方式,通过Object.
  • vue数据双向绑定是通过数据劫持结合发布者-订阅模式的方式来实现的.本文重点给大家介绍Vue数据双向绑定原理及简单实现方法,感兴趣的朋友跟随脚本之家小编一起学习吧
  • JS实现双向绑定

    2019-03-06 13:02:54
    现在前端MVVM思想盛行,通过对数据的改变从而引起页面的变化,如:Vue中实现双向绑定的原理就是:采用数据劫持结合发布者-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据...
  • Vue 3.0双向绑定原理的实现

    千次阅读 2019-10-21 17:19:01
    proxy方法 vue.js 是采用数据劫持结合发布者-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的...可查看Vue 2.0双向绑定原理的实现 具...
  • 在Vue开发中,一些大型项目或者...这时候,我就推荐使用全新的开发模式,发布-订阅模式,它是基于Vue的on,on,on,off,$emit这三个API开发。原理是创建一个事件中心绑定到每个组件上面,只要有一个组件的有新的数据发布

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,745
精华内容 4,298
关键字:

发布订阅模式实现双向绑定