Vue的响应式实现原理
MVVM
M:模型 ==》data中的数据
V:视图 ==》模板
VM:视图模型 ==》Vue实例对象
ViewModel是一个中间的桥梁将视图View与模型Model连接起来,ViewModel内部通过数据绑定,实现数据变化,视图发生更新变化,通过数据劫持实现的数据绑定;通过dom监听,实现事件触发,调用对应的回调函数,比如更新数据(数据变化了,视图就会更新–数据绑定)
Vue 的设计也受到了MVVM的启发,View对应的是dom,它的ViewModel对应的式Vue实例,,Model对应的是data对象;通过数据劫持来实现数据绑定;编译(事件指令)的时候添加事件监听
Vue的响应式原理
主要是:数据代理、数据绑定、模板编译
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKZWJxqy-1670162086242)(C:\Users\lucas\Desktop\学习\图片\reactive.jpg)]
绿线是初始化时执行的,红线是数据更新时触发的
一、初始化的时候:
1》数据代理:
数据代理就是通过一个对象代理对另一个对象中属性的操作(读/写);vue中通过vm代理data对象(vm代理vm._data)中所有属性的操作,更方便的操作data中的数据
vue中数据代理:将vue文件中的data保存一份到vm._data
; 然后对将vue文件中data中的每个属性添加到vm实例上,通过Object.defineProperty
实现数据的代理;当我们读取vm上的属性时,他会到vm._data
中找到对应的属性返回当,我们修改vue实例对象的属性后,对应的setter
就会监听到变化,然后去修改实例对象上vm._data
对应的属性
Object.defineProperty(me, key, {
configurable: false, // 不可以重新定义
enumerable: true, // 可枚举遍历
// 当执行vm.name获取属性值时自动调用返回属性值
get: function proxyGetter() {
// 读取data中对应的属性值返回
return me._data[key];
},
// 当执行vm.name = "xxx"时自动调用
set: function proxySetter(newVal) {
// 将最新的值保存给data对应的属性上
me._data[key] = newVal;
}
});
2》数据绑定:
数据绑定
初始化显示:页面(表达式、指令)能从data读取数据希纳是(编译、解析)
更新显示:更新data中的属性数据,能够更新页面
数据劫持
1》数据劫持是vue实现数据绑定的一种技术
2》基本思想:通过defineProperty()来监视data中所有属性(任意层次)数据的变化,一旦变化就更新界面
具体实现:
采用递归的方式为data中的每个层级的属性创建dep(实例对象),并通过defineProperty对data进行重新定义,实现数据劫持;在set中判断数据是否发生了变化,如果发生改变,一方面他会更新值,新的值是需要重新劫持监听;另一方面会通知所有相关订阅者watcher去更新界面;在get中不仅返回值,还需要建立watcher与dep的关系(这个get会在模板解析大括号表达式和指令时触发);给Watcher添加Dep,给watch的subs中push对应dep
dep对象:
{
id;0,
subs:[]
}
dep的id从0开始递增,每个属性对应一个dep,劫持
watcher对象:
{
vm:MVVM
exp:"name",
depIds:{depId:dep} // depId就是上面dep的id,是个数字
cb: textUpdater,
value:"luca"
}
watcher与dep的关系
多对多的关系
一个dep可能对应多个watcher; eg:一个属性在多个标签中使用
一个watcher可能的对应多个dep; eg: {{a.b.c}}
什么时候建立关系:编译、解析模板(大括号表达式和非事件指令)时建立
怎么建立关系:创建watcher时会读取data中的值,defineProperty中get会建立双方关系;在dep的subs中push了watcher,且在watcher的depIds中添加了对应dep
劫持的部分代码:
defineReactive: function (data, key, val) {
// 创建对应的dep对象
var dep = new Dep();
// 通过隐式递归调用, 实现对所有层次属性的劫持
var childObj = observe(val);
// 给data中指定属性进行重新定义
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
// 返回属性值, 同建立dep与watcher之间的关系
get: function () {
if (Dep.target) {
dep.depend();
}
return val;
},
// 监视属性值的变化, 一旦变化去更新对应的界面
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 是object的新的值话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
},
});
},
3》编译模板:
1、将el所有的子节点去除,添加到加到一个新建的文档 fragment 对象中
2、对 fragment中所有层次子节点递归进行编译解析处理
编译大括号表达式和一般指令时创建watcher;创建watcher时指定了更新函数(因为要更新的可能是a.b.c这种,所以要遍历,取值是从_data
中取的);这是初始化需要读取_data
中的数据,就会走对应属性的get,从而建立dep与watcher的关系,给dep的subs中push了对应的watcher,并且在watcher中的depIds添加了对应的dep
1》对表达式文本进行解析
a、根据正则对象匹配的表达式字符串( 匹配eg:{{obj.age}} ):子匹配
b、将属性值设置为文本节点的 textContent
c、从data中去除表达式对应的属性(通过.来分割成数组后遍历,因为可能由多层 a.b.c)
2》对元素节点的指令属性进行解析
a》 事件指令的解析
a.1、从指令中取出事件名
a2、根据指令的值从methods中得到对应的事件处理函数对象
a3、给当前元素节点绑定事件名和事件回调函数的事件监听
a4、指令解析完成后,移除次指令属性
b》一般指令的解析
b.1、从表达式中取出指令名和指令值
b.2、从data中根据表达式得到对应的值
b.3、根据指令名确定需要操作的元素节点的什么属性
b.4 、将表达式的值设置到对应的属性上(v-text:textContent;;v-html:innerHtml; v-class :className)
b5、移除元素的指令属性
3、将解析后的 fragment 添加到el中显示
fragment修改并不会引起页面的更新
二、数据变化的时候
监听到_data
的变化,set首先判断值有没有变化,发生变化而且是个对象的话会重新调用observe函数进行监听;通知所有相关的订阅者更新界面(通过遍历subs,调用watcher中的回调函数函数来更新界面)
双向数据绑定的实现
双向绑定是建立在单项数据绑定的基础上;只是在解析v-model指令时,给当前元素添加了input监听;当input的value发生改变时,修改对应的_data
中的属性
v-model 是动态属性绑定 v-bind 与 input事件的语法糖
关的订阅者更新界面(通过遍历subs,调用watcher中的回调函数函数来更新界面)
双向数据绑定的实现
双向绑定是建立在单项数据绑定的基础上;只是在解析v-model指令时,给当前元素添加了input监听;当input的value发生改变时,修改对应的_data
中的属性
v-model 是动态属性绑定 v-bind 与 input事件的语法糖