Vue.set的副作用有哪些(vue.set,web开发)

时间:2024-05-02 12:46:53 作者 : 石家庄SEO 分类 : web开发
  • TAG :

Vue虽然用挺久了,还是会踩到坑,来看下面这段很简单的?:点击a和b按钮,下面代码会提示什么?

<html><head><metacharset="utf-8"><scriptsrc="https://cdn.staticfile.org/vue/2.5.17/vue.min.js"></script></head><body><divid="app"><p>{{JSON.stringify(this.testObj)}}</p><button@click="set('a')">设置testObj属性a</button><button@click="set('b')">设置testObj属性b</button></div><script>newVue({el:'#app',data:{testObj:{},},watch:{'testObj.a'(){alert('a')},'testObj.b'(){alert('b')},},methods:{set(val){Vue.set(this.testObj,val,{});}},})</script></body></html>

答案是:

点a的时候alert a,点b的时候alert a,接着alert b。

如果再接着点a,点b,提示什么?

答案是:

点a的时候alert a,点b的时候alert b。

我们把代码做一个很小的改动:把Vue.set的值由对象改为true。这时候点击a和b按钮,下面代码会提示什么?

<html><head><metacharset="utf-8"><scriptsrc="https://cdn.staticfile.org/vue/2.5.17/vue.min.js"></script></head><body><divid="app"><p>{{JSON.stringify(this.testObj)}}</p><button@click="set('a')">设置testObj属性a</button><button@click="set('b')">设置testObj属性b</button></div><script>newVue({el:'#app',data:{testObj:{},},watch:{'testObj.a'(){alert('a')},'testObj.b'(){alert('b')},},methods:{set(val){Vue.set(this.testObj,val,true);}},})</script></body></html>

答案是:

点a的时候alert a,点b的时候alert b。

如果再接着点a,点b,提示什么?

答案是:

没有提示。

先总结一下发现的现象:用Vue.set为对象o添加属性,如果添加的属性是一个对象,那么o的所有属性会被触发响应。

是不是不明白?且请听我讲解一下。

要回答上面这些问题,我们首先需要理解一下Vue的响应式原理。

Vue.set的副作用有哪些

从Vue官网这幅图上我们可以看出:当我们访问data里某个数据属性p时,会通过getter将这个属性对应的Watcher加入该属性的依赖列表;当我们修改属性p的值时,通过setter通知p依赖的Watcher触发相应的回调函数,从而让虚拟节点重新渲染。

所以响不响应关键是看依赖列表有没有这个属性的watcher。

为了把依赖列表和实际的数据结构联系起来,我画出了vue响应式的主要数据结构,箭头表示它们之间的包含关系:

Vue.set的副作用有哪些

Vue里的依赖就是一个Dep对象,它内部有一个subs数组,这个数组里每个元素都是一个Watcher,分别对应对象的每个属性。Dep对象里的这个subs数组就是依赖列表。

从图中我们可以看到这个Dep对象来自于__ob__对象的dep属性,这个__ob__对象又是怎么来的呢?这就是我们new Vue对象时候Vue初始化做的工作了。Vue初始化最重要的工作就是让对象的每个属性成为响应式,具体则是通过observe函数对每个属性调用下面的defineReactive来完成的:

/***DefineareactivepropertyonanObject.*/functiondefineReactive(obj,key,val,customSetter,shallow){vardep=newDep();varproperty=Object.getOwnPropertyDescriptor(obj,key);if(property&&property.configurable===false){return}//caterforpre-definedgetter/settersvargetter=property&&property.get;if(!getter&&arguments.length===2){val=obj[key];}varsetter=property&&property.set;varchildOb=!shallow&&observe(val);Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:functionreactiveGetter(){varvalue=getter?getter.call(obj):val;if(Dep.target){dep.depend();if(childOb){childOb.dep.depend();if(Array.isArray(value)){dependArray(value);}}}returnvalue},set:functionreactiveSetter(newVal){varvalue=getter?getter.call(obj):val;/*eslint-disableno-self-compare*/if(newVal===value||(newVal!==newVal&&value!==value)){return}/*eslint-enableno-self-compare*/if(process.env.NODE_ENV!=='production'&&customSetter){customSetter();}if(setter){setter.call(obj,newVal);}else{val=newVal;}childOb=!shallow&&observe(newVal);dep.notify();}});}

让一个对象成为响应式其实就是给对象的所有属性加上getter和setter(defineReactive做的工作),然后在对象里加__ob__属性(observe做的工作),因为__ob__里包含了对象的依赖列表,所以这个对象就可以响应数据变化。

可以看到defineReactive里也调用了observe,所以让一个对象成为响应式这个动作是递归的。即如果这个对象的属性又是一个对象,那么属性对象也会成为响应式。就是说这个属性对象也会加__ob__然后所有属性加上getter和setter。

刚才说有没有响应看“依赖列表有没有这个属性的watcher”,但是实际上,ob 只存在属性所在的对象上,所以依赖列表是在对象上的依赖列表,通过依赖列表里Watcher的expression关联到对应属性(见图2)。说以准确的说:有没有响应应该是看“对象的依赖列表里有没有属性的watcher”。

注意我们在data里只定义了testObj空对象,testObj并没有任何属性,所以testObj的依赖列表一开始是空的。

但是因为代码有定义Vue对象的watch,初始化代码会对每个watch属性新建watcher,并添加到testObj的依赖队列__ob__.dep.subs里。这里的添加方法非常巧妙:新建watcher时候会一层层访问watch的属性。比如watch 'testObj.a',vue会先访问testObj,再访问testObj.a。因为testObj已经初始化成响应式的,访问testObj时会调用defineReactive里定义的getter,getter又会调用dep.depend()从而把testObj.a对应的watcher加到依赖队列__ob__.dep.subs里。于是新建watcher的同时完成了把watcher自动添加到对应对象的依赖列表这个动作。

小结一下:Vue对象初始化时会给data里对象的所有属性加上getter和setter,添加__ob__属性,并把watch属性对应的watcher放到__ob__.dep.subs依赖列表里。

所以经过初始化,testObj的依赖列表里已经有了属性a和b对应的watcher。

有了以上基础知识我们再来看Vue.set也就是下面的set函数做了些什么。

/***Setapropertyonanobject.Addsthenewpropertyand*triggerschangenotificationifthepropertydoesn't*alreadyexist.*/functionset(target,key,val){if(process.env.NODE_ENV!=='production'&&(isUndef(target)||isPrimitive(target))){warn(("Cannotsetreactivepropertyonundefined,null,orprimitivevalue:"+((target))));}if(Array.isArray(target)&&isValidArrayIndex(key)){target.length=Math.max(target.length,key);target.splice(key,1,val);returnval}if(keyintarget&&!(keyinObject.prototype)){target[key]=val;returnval}varob=(target).__ob__;if(target._isVue||(ob&&ob.vmCount)){process.env.NODE_ENV!=='production'&&warn('AvoidaddingreactivepropertiestoaVueinstanceoritsroot$data'+'atruntime-declareitupfrontinthedataoption.');returnval}if(!ob){target[key]=val;returnval}defineReactive(ob.value,key,val);ob.dep.notify();returnval}

我们关心的主要就最后这两句:defineReactive(ob.value, key, val); 和ob.dep.notify();。

defineReactive的作用就是让一个对象属性成为响应式。ob.dep.notify()则是通知对象依赖列表里面所有的watcher:数据变化了,看看你是不是要做点啥?具体做什么就是图2 Watcher里面的cb。当我们在vue 里面写了 watch: { p: function(oldValue, newValue) {} } 时候我们就是为p的watcher添加了cb。

所以Vue.set实际上就做了这两件事:

  • 把属性变成响应式 。

  • 通知对象依赖列表里所有watcher数据发生变化。

那么问题来了,既然依赖列表一直包含a和b的watcher,那应该每次Vue.set时候,a和b的cb都应该被调用,为什么结果不是这样呢?奥妙就藏在下面的watcher的run函数里。

/***Schedulerjobinterface.*Willbecalledbythescheduler.*/Watcher.prototype.run=functionrun(){if(this.active){varvalue=this.get();if(value!==this.value||//DeepwatchersandwatchersonObject/Arraysshouldfireeven//whenthevalueisthesame,becausethevaluemay//havemutated.isObject(value)||this.deep){//setnewvaluevaroldValue=this.value;this.value=value;if(this.user){try{this.cb.call(this.vm,value,oldValue);}catch(e){handleError(e,this.vm,("callbackforwatcher\""+(this.expression)+"\""));}}else{this.cb.call(this.vm,value,oldValue);}}}};

dep.notify通知watcher后,watcher会执行run函数,这个函数才是真正调用cb的地方。我们可以看到有这样一个判断 if (value !==this.value || isObject(value) || this.deep) 就是说值不相等或者值是对象或者是深度watch的时候,都会触发cb回调。所以当我们用Vue.set给对象添加新的对象属性的时候,依赖列表里的每个watcher都会通过这个判断(新添加属性因为{} !== {} 所以value !==this.value成立,已有属性因为isObject(value)),都会触发cb回调。而当我们Vue.set给对象添加新的非对象属性的时候,只有新添加的属性通过value !==this.value 判断会触发cb,其他属性因为值没变所以不会触发cb回调。这就解释了为什么第一次点击按钮b的时候场景一和场景二的效果不一样了。

那既然依赖列表没变为什么第二次点击按钮效果就不一样了呢?

这就是set函数里面这个判断起的作用了:

if(keyintarget&&!(keyinObject.prototype)){target[key]=val;returnval}

这个判断会判断对象属性是否已经存在,如果存在的话只是做一个赋值操作。不会走到下面的defineReactive(ob.value, key, val); 和ob.dep.notify();里,这样watcher没收到notify,就不会触发cb回调了。那第二次点击按钮的回调是哪里触发的呢?还记得刚才的defineReactive里定义的setter吗?因为testObj已经成为了响应式,所以进行属性赋值操作会触发这个属性的setter,在set函数最后有个dep.notify();就是它通知了watcher从而触发cb回调。

就算是这样第二次点击不是应该a和b都触发的吗?依赖列表不是一直包含有a和b的watcher吗?

这里就要涉及到另一个概念“依赖收集”,不同于__ob__.dep.subs这个依赖列表,响应式对象还有一个依赖列表,就是defineReactive里面定义的var dep,每个属性都有一个dep,以闭包形式出现,我暂且称它为内部依赖列表。在前面的set函数判断里,判断通过会执行target[key]= val; 这句赋值语句会首先触发getter,把属性key对应的watcher添加到内部依赖列表,这个步骤就是Vue官网那张图里的“collect as dependencies”;然后触发setter,调用dep.notify()通知watcher执行watcher.run。因为这时候内部依赖列表只有一个watcher也就是属性对应的watcher。所以只触发了属性本身的回调。

根据以上分析我们还原一下两个场景:

场景1:Vue.set 一个对象属性

  • 点击按钮a: Vue.set把属性a变成响应式,通知依赖列表数据变化,依赖列表中watcher-a发现数据变化,执行a的回调。

  • 点击按钮b: Vue.set把属性b变成响应式,通知依赖列表数据变化,依赖列表中watcher-a发现a是对象,watcher-b发现数据变化,均满足触发cb条件,于是执行a和b的回调。

  • 再点击按钮a: Vue.set给a属性赋值,触发getter收集依赖,内部依赖列表收集到依赖watcher-a,触发setter通知内部依赖列表数据变化,watcher-a发现数据变化,执行a的回调。

  • 再点击按钮b: Vue.set给b属性赋值,触发getter收集依赖,内部依赖列表收集到依赖watcher-b,触发setter通知内部依赖列表数据变化,watcher-b发现数据变化,执行b的回调。

场景2:Vue.set 一个非对象属性

  • 点击按钮a: Vue.set把属性a变成响应式,通知依赖列表数据变化,依赖列表中watcher-a发现数据变化,执行a的回调。

  • 点击按钮b: Vue.set把属性b变成响应式,通知依赖列表数据变化,watcher-b发现数据变化,执行b的回调。

  • 再点击按钮a: Vue.set给a属性赋值,触发getter收集依赖,内部依赖列表收集到依赖watcher-a,触发setter,发现数据没变化,返回。

  • 再点击按钮b: Vue.set给b属性赋值,触发getter收集依赖,内部依赖列表收集到依赖watcher-b,触发setter,发现数据没变化,返回。

原因总结:

  • Vue响应式对象有内部、外部两个依赖列表。

  • Vue.set有添加属性、修改属性两种功能。

  • Watcher在判断是否需要触发回调时有对象属性、非对象属性的区别。

 </div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
本文:Vue.set的副作用有哪些的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:Node.js中模块加载机制的原理是什么下一篇:

10 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18