关于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等其他指令
内容来源于网络 博主自己整合加修改的。
仅作为学习交流使用。
如有侵权 请联系删除