vue.js响应式数据绑定原理

响应式数据绑定是vue.js的核心之一,该功能的实现主要是利用Object.defineProperty方法。

  1. Object.defineProperty()

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。 存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。这里主要是通过操作存取描述符来实现某些功能。

下面简单的来实现一个hello存取访问器

var obj = {};
Object.defineProperty(obj, 'hello', {
    get: function(){
	    console.log('调用了get取值属性');
	},
	set: function(v){
		console.log('调用了set存值属性,值为:' + v);
	}
})

obj.hello;  //调用了get取值属性
obj.hello = 'How are you?'; // 调用了set存值属性,值为:How are you?
复制代码

这里的存取描述符也就是常说的访问属性,也就是对象内部的get和set方法,注意的是,访问器属性会覆盖同名的普通属性,因为访问器属性的优先级高于普通属性。

下面来实现一个极简版数据双向绑定效果

<input type="text" id="a" />
<span id="b"></span>
复制代码
var obj = {};
Object.defineProperty(obj, 'hello', {
	set: function(newVal){
		document.getElementById('a').value = newVal;
		document.getElementById('b').innerHTML = newVal;
	}
})

document.addEventListener('keyup',function(e){
	obj.hello = e.target.value;
})
复制代码

  1. 上述是解析原理的简单案例,接下来我们一点一点,由外到内,来解析一个简单的vue案例,代码如下
<div id="app">
	<input type="text" id="a1" v-model="text" />
	{{text}}
</div>
复制代码
var vm = new Vue({
	el: "app",
	data: {
		text: 'hello world'
	}
})
复制代码

首先,这里有两个模块大的解析,一个是vue代码的解析,一个是DOM元素包括vue指令和插值语法的解析,我们先来看Vue的构造函数。 Vue构造函数主要包含两个功能,观察vue实例的data数据和编译DOM元素,最后把编译的内容插入到vue挂载的实例中。

function Vue(options){
	this.data = options.data;
	var data = this.data;
	observe(data, this); // 解析vue实例的data
	
	var id = options.el;
	var dom = nodeToFragment(document.getElementById(id), this); //解析dom和vue指令
	document.getElementById(id).appendChild(dom);
}
复制代码

这里的DocumentFragment(文档片段)可以看作节点的容器,当我们把它追加到某个节点上时,只有子节点会追加进去。

现在来看observe函数,这个主要是用来遍历vue实例的data数据

function observe(obj, vm){
	Object.keys(obj).forEach(function(key){
		defineReactive(vm, key, obj[key]);
	})
}
复制代码

函数里面调用defineReactive函数,用户响应式绑定相关数据

function defineReactive(obj, key, val){
	var dep = new Dep();
	Object.defineProperty(obj, key, {
		get: function(){
			//如果有新new的Watcher对象就往dep栈里面添加
			if(Dep.target) dep.addSub(Dep.target);
			return val;
		},
		set: function(newVal){
			if( newVal === val) return
			val = newVal;
			dep.notify(); // 变更数据,发送notify
			console.log(val);
		}
	})
}
复制代码

接下来看第二个模块,解析DOM建议vue指令,定义nodeToFragment函数

function nodeToFragment(node, vm){
	var flag = document.createDocumentFragment();
	var child;
	while(child = node.firstChild){ // 循环第一个子元素
		compile(child, vm); // 解析vue指令和插值语法
		flag.appendChild(child); // 执行appendChild后会移除该子元素
	}
	return flag;
}
复制代码

compile函数负责编译vue指令和插值语法并监听input输入的变动。

function compile(node, vm){
	var reg = /\{\{(.)\}\}/; // 匹配双括号插值
	if(node.nodeType === 1){
		var attr = node.attributes;
		for(var i = 0; i < attr.length; i++){
			if(attr[i].nodeName == 'v-model'){
				var name = attr[i].nodeValue;
				node.addEventListener('input', function(e){
					vm[name] = e.target.value;
				})
				node.value = vm[name];
				node.removeAttribute('v-model');
			}
		}
		new Watcher(vm, node, name, 'input'); // 节点编译会为每一个节点添加一个订阅
	}
	if(node.nodeType === 3){ //文本节点
		if(reg.test(node.nodeValue)){
			var name = RegExp.$1;
			name = name.trim();
			new Watcher(vm, node, name, 'text'); //节点编译会为每一个节点添加一个订阅
		}
	}
}

复制代码

最后,我们来看一下vue的订阅发布模式,既然是发布订阅,肯定会有发布方和订阅方,发布一般都是在特定的条件下触发的,所以这里先看订阅方

//参数说明:vue对象  添加订阅的节点  对应vue对象date的key 节点类型
function Watcher(vm, node, name, nodeType){
  Dep.target = this; // 把当前new的Watcher对象赋予给全局对象Dep.target
  this.name = name;
	this.node = node;
	this.vm = vm;
	this.nodeType = nodeType;
	this.update(); // 执行update方法 下方prototype定义过
	Dep.target =  null;
}

Watcher.prototype = {
	update: function(){ // 先获取vm的值 然后更新到节点上
		this.get();
		if(this.nodeType == 'text'){
			this.node.nodeValue = this.value;
		}
		if(this.nodeType == 'input'){
			this.node.vlaue = this.value;
		}
	},
	get: function(){
		this.value = this.vm[this.name];
	}
}
复制代码

我们需要一个全局的主题对象,方便随时发布和订阅

function Dep(){
	this.subs = []; // Watcher监听栈
}
Dep.prototype = {
	addSub: function(sub){
		this.subs.push(sub);
	},
	notify: function(){
		this.subs.forEach(function(sub){
			sub.update();
		})
	}
};
复制代码

联系上面区块的代码,在上面vue data数据观察阶段,调用访问属性每取一个属性的值就会向主题栈中添加一个Watcher监听对象 而这个Watcher监听对象在编译DOM阶段通过new操作生成,调用访问属性每设置一个属性的值,表明数据已经变更,发送消息,告知主题对象 原数据已经变更,调用订阅对象Watcher执行更新操作。

至此,一个简单的vue小案例代码解析完成,双向绑定的效果也实现完成。

本文参考:www.cnblogs.com/kidney/p/60…

转载于:https://juejin.im/post/5cb1b7055188251acc53921a

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值