前言
vue通过各个模块的协调配合,来侦测数据的变化,以便对使用数据的地方(以下简称依赖)进行响应式的更新:
本问将围绕下面的例子,来阐述实时侦测的基本原理:
vm.$watch('a,b,c',function(newValue,oldValue){
//这里的代码可以时DOM操作,实时更新页面对vm.a.b.c有依赖的部分;
})
//上述代码的作用在于,当vm.a.b.c属性发生值变化时,调用相应的回调,
阅读本文,你或许应该具备的Js基础知识:
- Js的访问器属性
- 函数闭包的原理
一、vue如何实时的侦测数据变化?
vue如何实时的侦测数据变化,对于本文来说就是如何实时的侦测属性vm.a.b.c的变化。实质上,这是通过把对象的数据属性替换成同名的访问器属性来实现的,即通过Object.defineProperty(obj,key,{...});
:
//对本文而言,创建一个同名的访问器属性:
Object.defineProperty(vm,'a,b,c',{
enumerable : true,
configurable : true,
get function(){
//读取该访问器属性时需执行的代码体;
},
set function(newValue){
//试图写入该访问器属性时,需要执行的代码体;
}
});
//于是,原有的数据属性被同名的访问器属性替代,即依然存在vm.a.b.c,但是该属性是新添加的访问器属性;
但是,我们必须还得把原来的数据属性值保留下来,于是我们可以封装以上的代码:
function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
Object.defineProperty(vm,'a,b,c',{
enumerable : true,
configurable : true,
get function(){
//读取该访问器属性时需执行的代码体:
return value;
},
set function(newValue){
//试图写入该访问器属性时,需要执行的代码体:
if( value === newValue ){
return ;
}
value = newValue ;
}
});
}
//set和get内引用了value,所以即使函数执行完毕,value也不会被回收,只要访问器属性还存在,它就会一直存在于内存中,和函数闭包的原理一致;
上述代码的意义在于,把数据属性用同名访问器属性所替代,并且,访问器属性有以下的特点:
- 当读取时,执行get函数;
- 当写入值时,执行set函数;
- 类似于一个对特定行为(读或写)执行特定操作的执行器;
最重要的是前两个特点,可以起到以下的功能:
- 依赖必定会读取数据(在本文中即读取vm.a.b.c)。而对访问器属性而言,读取数据就等同于执行get函数。于是我们可以利用这一点,在get函数体内收集相关的依赖。
- 当我们试图写入数据时。同上,对访问器属性而言,写入数据等同于执行set函数。利用这一点,我们便可以在set函数体内,触发上面收集到的依赖(例如封装了DOM操作的函数),以便实时的更新相关的地方(例如页面的相关部分)。
二、如何收集依赖?
我们要在get中收集依赖,或许更准确的说我们在本文中所收集的依赖就是那个回调函数,它会执行某些操作(例如DOM操作),从而更新页面中与vm.a.b.c相关联的地方。最原始的方式是通过一个数组实现:
代码如下(示例):
function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
let dep = []; //用于收集依赖
Object.defineProperty(vm,'a,b,c',{
enumerable : true,
configurable : true,
get function(){
//读取该访问器属性时需执行的代码体:
dep.push(window.target); //window.target被用于暂存依赖(本文中就是暂存回调函数);
return value;
},
set function(newValue){
//试图写入访问器属性vm.a.b.c时,其实写入的值是作为set function的第一个参数;
//试图写入该访问器属性时,需要执行的代码体:
if( value === newValue ){
return ;
}
for(let i = 0 ; i < dep.length ; i++){
dep[i](newValue,value); //当写入新值时,触发回调函数来执行相应的操作(例如DOM操作);
}
value = newValue ;
}
});
}
//相当于函数闭包,dep在函数执行完后依然存在,它的生命周期即是vm.a.b.c的生命周期;
但是上述的操作有些许耦合,事实上我们可以把依赖收集的代码封装成一个 Dep类,它专门帮我们管理依赖。使用这个类,我们可以收集依赖,删除依赖或者向依赖发送通知(调用回调函数)。
代码如下(示例):
class Dep{
constructor(){
this.subs = [] ; //用于存储依赖(回调函数);
}
addSub (sub) { //用于对数组添加依赖;
this.subs.push(sub);
}
removeSub (sub) {
remove(this.subs,sub);
}
depend () {
if (window.target){
this.addSub(window.target);
}
}
notify () {
const subs = this.subs.slice();
for(let i = 0 , l = subs.length ; i < 1 ; i++){
subs[i].update() ; //update方法用于执行相应的操作,稍后定义;
}
}
}
function remove (arr , item){
if(arr.length){
const index = arr.indexOf(item);
if( index > -1 ){
return arr.splice(index,1);
}
}
}
function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
let dep = new Dep(); //用于收集依赖
Object.defineProperty(vm,'a,b,c',{
enumerable : true,
configurable : true,
get function(){
//读取该访问器属性时需执行的代码体:
dep.depend() ; //window.target被用于暂存依赖(本文中就是暂存回调函数);
return value;
},
set function(newValue){
//试图写入访问器属性vm.a.b.c时,其实写入的值是作为set function的第一个参数;
//试图写入该访问器属性时,需要执行的代码体:
if( value === newValue ){
return ;
}
value = newValue ;
dep.notify();
}
});
}
2.读入数据
代码如下(示例):
总结
提示:这里对文章进行总结: