vue双向数据绑定原理


关于vue的双向数据绑定 相信很多人都知道是通过Object.defineProperty属性拦截的方法,将vue的data对象里面的每个数据读写 转换成getter/setter的方式来实现的,当数据变化的时候 就通知视图更新 一句话概括了 但是关于其内部的实现 还是值得深究的

双向数据绑定

所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新节点数据
在这里插入图片描述
也就是当页面输入框中的内容变化的时候,data里面的数据同步更新,也就是view–>model的过程
data中的数据变化的时候 页面文本节点上的数据同步更新,也就是model–>view的过程

要实现上面这两个变化,首先应该该清楚,数据在什么时候变化的,如果数据的变化变得可监测,那这个问题就很简单了

数据观测器 Observer

数据是怎么被劫持的呢 直接撸代码 看下面

var vm = new Vue({
	data:{
		person:{
			"name":"xiaoming"
		}
	},
	created:function (){
		console.log(this.person)
	}
});

看下相关打印
在这里插入图片描述
这里就能看到person有两个相对应的方法,get和set,为什么会多出这两个方法呢,这是因为vue通过Object.defineProperty()实现了数据的劫持,Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

一般情况下 我们获取属性的操作是如下这种

let person = {name:"小明"};
console.log(person.name);//打印结果:小明

为了让person的name这个属性变的可以被监测到 下面做个改变

let person= {}
let name = '小明'
Object.defineProperty(person, 'name', {
    get(){
        console.log('name属性被读取了')
        return person['name'];
    },
    set(newVal){
        console.log('name属性被修改了')
        person['name'] = newVal;
    }
})

然后输入person.name,可以看到name属性被读取了 也就是调用了get的方法
在这里插入图片描述
输入 person.name = “小红”,会看到name的属性被改写了 调用了set的方法
在这里插入图片描述

至此我们已经能监测到person里面属性的变化了 为了能监测到person所有属性的变化 我们做个改变

var person = {name:"小明",age:16};
//劫持数据并给所有数据属性添加get和set的方法,
function defineReactive(data,key,value){
	Object.defineProperty(data,key,{
		set:function (newVal){
			value = newVal;
			console.log(`设置的key:${key},value:`+value);
		},
		get:function (){
			console.log(`获取的key:${key},value:`+value);
			return value;
		}
	});
}
//遍历对象,让每一个属性都会被上面那步劫持到
function observer(data){
	Object.keys(data).forEach(function (key){
		defineReactive(data,key,data[key]);
	});
}
observer(person);

这样 person的两个属性 name和age都变得可以监测了。这样就能知道数据在什么时候被获取,被修改了。
但是 如果我给person添加一个sex属性呢?
在这里插入图片描述
会发现 新添加的这个属性,并没有触发set方法
同样再去获取这个新添加的属性
在这里插入图片描述
也是没有触发get方法的 这是为什么呢?
因为监测器observer监测的是对象一开始的已有的属性,添加属性之后,这个新添加的属性 并没有被上述的方法重新劫持绑定,添加对应的get和set方法,所以新加的属性没有被监测到变动
【有兴趣的同学可以深究下为什么新添加的数据不会被监测,这里暂不做扩展,可以研究下vue关于数组的实现,和数组的方法实现】

消息订阅器Dep

现在我们知道数据是什么时候变化的了,但是怎么通知依赖改数据的视图实现更新呢?
为了方便 我们会将所有的依赖先收集起来,一旦数据发生变化,就一起通知更新
这样 我们就需要创建一个依赖收集的容器,也就是消息订阅器Dep,用来容纳所有的订阅者,订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数
创建一个消息订阅器 如下:

function Dep(){
	this.subs = [];
}
Dep.prototype = {
	addSub:function (sub){
		this.subs.push(sub);
	},
	//判断是否增加订阅者
	depend:function () {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    },
	 //通知订阅者更新
	notify:function(){
         this.subs.forEach((sub) =>{
             sub.update()
         })
     }
}
Dep.target = null;

因为只需要在订阅者初始化的时候才需要添加订阅者,所以我们在上面的订阅器做了个处理,在Dep.target上缓存订阅者,添加成功之后 再将其去掉。
有了这个订阅器之后 再将上面的defneReactive函数改造下,增加订阅器

function defineReactive(data,key,value){
	let dep = new Dep();
	Object.defineProperty(data,key,{
		set:function (newVal){
			value = newVal;
			console.log(`设置的key:${key},value:`+value);
			dep.notify();//数据变化 通知订阅者
		},
		get:function (){
			dep.depend();
			console.log(`获取的key:${key},value:`+value);
			return value;
		}
	});
}

这样get的时候 判断是否增加订阅者 如果有增加 就在set的时候 通知订阅者,这样订阅者收到消息 就去执行对应的函数更新

下面我们来实现下订阅者watcher 用来管理所有的消息订阅器

订阅者 Watcher

function Watcher(vm,exp,cb){
	this.cb = cb;//wtcher绑定的更新函数
	this.vm = vm;//vue实例对象
	this.exp = exp;//node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name
	this.value = this.get();//将自己添加到订阅器
}
Watcher.prototype = {
	update:function (){
		let value = this.vm.data[this.exp];
		let oldValue= this.value;//缓存旧的value
		if(value!=oldValue){//数据发生变化,重新赋值
			this.value = value;
			this.cb.call(this.vm,value,oldValue);
		}
	},
	get:function (){
		Dep.target = this;//将自己缓存起来
		let value = this.vm.data[this.exp];//获取属性值会强制执监测器的get函数
		Dep.target = null;//获取属性值之后释放自己
		return value;
	}
}

分析一下上面的过程
1.当我们实例化一个Watcher的时候,首先进入构造函数,执行this.get()方法
2.首先会执行Dep.target = this 缓存自己, 实际上就是把Dep.target赋值为当前的渲染watcher
3.接着执行let value = this.vm.data[this.exp]; 这个过程会对vm的数据进行访问,也就是为了触发data数据的get函数
4.每个get函数里面 都有一个dep 获取属性 触发get就就会调用dep.depend(); 也就会执行this.addSub(Dep.target),也就是把当前的watcher订阅到这个数组持有的dep的subs中,这个的目的是为了后续数据变化的时候能通知到哪些subs做准备
5.这样其实已经完成了一个依赖收集的过程了,但是还需要将Dep.target=null 释放自己,恢复成初始的状态。因为当前vm的数据依赖已经收集完成了,对应的Dep.target也需要改变
6 update函数 是用来当数据变化的时候调用自身的watch函数进行更新的操作,先通过let value = this.vm.data[this.exp]; 获取到最新的数据
7.然后将这个最新的数据与之前的数据进行比较,如果不一致,就调用更新函数cb进行更新

至此,一个简单的订阅者Watcher就实现了
总结
实现数据的双向绑定,首先要对数据进行劫持监测,所以我们需要设置一个监测器Observer,用来监测所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监测器Observer和订阅者Watcher之间进行统一管理的。
关系如图:
在这里插入图片描述

结合上面的Observer和Watcher和Dep 简单测试下

<body>
<input />
<br/>
<h1 id="name"></h1>
//实际页面逻辑
function myVue(data,el,exp){
	this.data = data;
	observer(data);
	el.innerHTML = this.data[exp];
	new Watcher(this,exp,function (value){
		console.log(value);
		el.innerHTML = 	'变化后的数据:'+value;
	});
	return this;
}

var ele = document.querySelector("#name");
var input= document.querySelector("input");
var vm = new myVue({name:"初始数据"},ele,'name');
input.oninput = function (e){
	vm.data.name = e.target.value;
}

在input输入内容 h1的内容会跟着改变,实现数据的实时更新了。
但是此处的解析dom的操作是手动去解析的 并不是自动解析的 ,要想自动解析对应的dom上面的指定 转成可被监测的指令 这个过程是怎么实现的呢?

解析器 Compile

他的作用是1.解析模板指令,并替换模板数据,更新视图,2.将模板指定对应的节点绑定对应的更新函数,并初始化响应的订阅器

为了解析模板 首先需要获取dom,然后对dom元素上含有的指令的节点做处理,因此这个环节会对dom操作比较频繁。
他会先建一个fragment片段,然后将需要处理的dom节点存入这个片段 再来处理

//将需要处理的dom节点存入片段
nodeToFragent(el){
	var fragment = document.createDocumentFragment();
	var child = el.firstChild;
	while(child){
		//将dom元素移入fragment
		fragment.appendChild(child);
		child = el.firstChild;
	}
	return fragment;
}

下一步 需要遍历各个节点,执行下面的compileElement 对含有相关指令的节点 进行特殊的处理
先处理个简单的双括号赋值{{}} 这个指令的处理

compileElement  (el){
	var childNodes = el.childNodes;
	var self = this;
	[].slice.call(childNodes).forEach(function(node){
		var reg = /\{\{(.*)\}\}/;
		var text = node.textContent;
		//【如果需要扩展处理其他指令也是在此处添加另外的if来处理】
		if(node.nodeType == 3 && reg.test(text)){// 判断是否是文本节点 同时符合这种形式{{}}的指令
			self.compileText(node, reg.exec(text)[1]);
		}
		if (node.childNodes && node.childNodes.length) {
			self.compileElement(node);  // 继续递归遍历子节点
		}
	});
},
compileText (node, exp) {
	var self = this;
	var initText = this.vm.data[exp];//拿到data对象里面的初始数据
	this.updateText(node, initText);  // 步骤1:将初始化的数据初始化到视图中
	new Watcher(this.vm, exp, function (value) {  // 步骤2:生成订阅器并绑定更新函数
		self.updateText(node, value);//更新
	});
},
updateText(node, value) {
	node.textContent = typeof value == 'undefined' ? '' : value;
},

获取到最外层节点后,调用compileElement函数,对所有子节点进行判断,如果节点是文本节点且匹配 {{}} 这种形式指令的节点就开始进行编译处理,编译处理首先需要初始化视图数据,对应上面所说的步骤1,接下去需要生成一个绑定更新函数的订阅器,对应上面所说的步骤2。这样就完成指令的解析、初始化、编译三个过程,一个解析器Compile也就可以正常的工作了。

对测试页面部分代码做下改变
//html部分

<div id="app">
	<h1>{{name}}</h1>
	<h2>{{title}}</h2>
</div>

//js部分

function myVue(options){
	this.data = options.data;
	observe(this.data);	
	new Compile(options.el, this);
	return this;
}
var vm = new myVue({
	el:"#app",
	data:{
		title:"hello-title",
		name:"hello-name"
	}
})
setTimeout(function (){
	vm.data.title = "改变之后的title";
	vm.data.name = "改变之后的title";
},3000);

再结合上面的解析器,一个完整的双向数据绑定的例子就完成了
刚开始显示的是初始的值“hello-title”,3s之后 显示直接变成了“改变之后的title”;这个数据已经做到了实时更新

有兴趣的同学可以在这个上面扩展,比如v-model v-on等其他指令

内容来源于网络 博主自己整合加修改的。
仅作为学习交流使用。
如有侵权 请联系删除

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值