我的编程空间,编程开发者的网络收藏夹
学习永远不晚

有哪些使用Vue.set的副作用

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

有哪些使用Vue.set的副作用

本篇内容主要讲解“有哪些使用Vue.set的副作用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“有哪些使用Vue.set的副作用”吧!

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

<html> <head>     <meta charset="utf-8">     <script class="lazy" data-src="https://cdn.staticfile.org/vue/2.5.17/vue.min.js"></script> </head>  <body> <div id="app">     <p>{{ JSON.stringify(this.testObj) }}</p>     <button @click="set('a')">设置testObj属性a</button>     <button @click="set('b')">设置testObj属性b</button> </div>  <script>   new Vue({     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>     <meta charset="utf-8">     <script class="lazy" data-src="https://cdn.staticfile.org/vue/2.5.17/vue.min.js"></script> </head>  <body> <div id="app">     <p>{{ JSON.stringify(this.testObj) }}</p>     <button @click="set('a')">设置testObj属性a</button>     <button @click="set('b')">设置testObj属性b</button> </div>  <script>   new Vue({     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来完成的:

 function defineReactive (   obj,   key,   val,   customSetter,   shallow ) {   var dep = new Dep();    var property = Object.getOwnPropertyDescriptor(obj, key);   if (property && property.configurable === false) {     return   }    // cater for pre-defined getter/setters   var getter = property && property.get;   if (!getter && arguments.length === 2) {     val = obj[key];   }   var setter = property && property.set;    var childOb = !shallow && observe(val);   Object.defineProperty(obj, key, {     enumerable: true,     configurable: true,     get: function reactiveGetter () {       var value = getter ? getter.call(obj) : val;       if (Dep.target) {         dep.depend();         if (childOb) {           childOb.dep.depend();           if (Array.isArray(value)) {             dependArray(value);           }         }       }       return value     },     set: function reactiveSetter (newVal) {       var value = getter ? getter.call(obj) : val;              if (newVal === value || (newVal !== newVal && value !== value)) {         return       }              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函数做了些什么。

 function set (target, key, val) {   if (process.env.NODE_ENV !== 'production' &&     (isUndef(target) || isPrimitive(target))   ) {     warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));   }   if (Array.isArray(target) && isValidArrayIndex(key)) {     target.length = Math.max(target.length, key);     target.splice(key, 1, val);     return val   }   if (key in target && !(key in Object.prototype)) {     target[key] = val;     return val   }   var ob = (target).__ob__;   if (target._isVue || (ob && ob.vmCount)) {     process.env.NODE_ENV !== 'production' && warn(       'Avoid adding reactive properties to a Vue instance or its root $data ' +       'at runtime - declare it upfront in the data option.'     );     return val   }   if (!ob) {     target[key] = val;     return val   }   defineReactive(ob.value, key, val);   ob.dep.notify();   return val }

我们关心的主要就最后这两句: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函数里。

 Watcher.prototype.run = function run () {   if (this.active) {     var value = this.get();     if (       value !== this.value ||       // Deep watchers and watchers on Object/Arrays should fire even       // when the value is the same, because the value may       // have mutated.       isObject(value) ||       this.deep     ) {       // set new value       var oldValue = this.value;       this.value = value;       if (this.user) {         try {           this.cb.call(this.vm, value, oldValue);         } catch (e) {           handleError(e, this.vm, ("callback for watcher \"" + (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 (key in target && !(key in Object.prototype)) {   target[key] = val;   return val }

这个判断会判断对象属性是否已经存在,如果存在的话只是做一个赋值操作。不会走到下面的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在判断是否需要触发回调时有对象属性、非对象属性的区别。

结论:

  • 用Vue.set添加对象属性,对象的所有属性都会触发一次响应。

  • 用Vue.set修改对象属性,只有当前修改的属性会触发一次响应。

我个人觉得Vue.set这种添加和修改不一致的表现是vue的一个缺陷。还没看Vue 3.0代码,看过的朋友可以告诉我下,是不是也有这样的问题?

规避方法:

添加一个对象属性会让所有属性触发响应这个特性应该不是我们想要的效果。目前没想到好的解决方法,只能在data里定义对象时先把对象的属性全写上。避免使用Vue.set设置对象属性。

到此,相信大家对“有哪些使用Vue.set的副作用”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

有哪些使用Vue.set的副作用

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

使用ThreadLocal的作用有哪些

本篇文章为大家展示了使用ThreadLocal的作用有哪些,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,
2023-05-31

使用HttpServletRequest对象的作用有哪些

这篇文章将为大家详细讲解有关使用HttpServletRequest对象的作用有哪些,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。  使用HttpServletRequest可以防止盗链行为
2023-05-31

使用JML 改进Java程序有什么副作用

这篇文章主要为大家展示了“使用JML 改进Java程序有什么副作用”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“使用JML 改进Java程序有什么副作用”这篇文章吧。副作用回忆一下代码段2中po
2023-06-03

powershell的作用有哪些

今天小编给大家分享一下powershell的作用有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。powershell能干
2023-07-05

Jqgrid的作用有哪些

JqGrid是一个基于jQuery的表格插件,用于在网页上展示和操作数据。它具有以下作用:1. 数据展示:JqGrid可以将数据以表格的形式展示在网页上,提供分页、排序、过滤等功能,方便用户浏览和查找数据。2. 数据编辑:JqGrid支持对
2023-09-11

Vuex的作用有哪些

这篇文章给大家分享的是有关Vuex的作用有哪些的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。概述想必用过 vue.js 的童鞋,一定知道在 vue 各个组件之间传值的痛苦,基于父子、兄弟组件,我们传值可能会很方便
2023-06-15

Kotlin的作用有哪些

Kotlin的作用有以下几个:Android开发:Kotlin可以用于开发Android应用程序,它与Java语言兼容,可以与现有的Java代码无缝集成。服务器端开发:Kotlin可以用于开发服务器端应用程序,它提供了许多方便的特性,如函数
2023-10-25

Python的作用有哪些

本篇内容主要讲解“Python的作用有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python的作用有哪些”吧!Python能干什么?1、web开发领域2、机器学习领域3、游戏开发领域4、
2023-06-15

jQuery的作用有哪些

今天小编给大家分享一下jQuery的作用有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。jQuery是什么?jQuery
2023-06-27

github的作用有哪些

本篇内容主要讲解“github的作用有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“github的作用有哪些”吧!github的作用:1、让开发变得更简单;github能够在服务器上存储代码
2023-07-05

phpinclude的作用有哪些

1. 代码重用:phpinclude可以将一个PHP文件中的代码引入到另一个PHP文件中,实现代码的重用。2. 维护性:将具有相同功能的代码放在一个文件中,方便维护和修改。3. 可读性:将不同的代码放在不同的文件中,可以提高代码的可读性和可
2023-06-13

css3的作用有哪些

本篇文章为大家展示了css3的作用有哪些,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。CSS3是CSS2的升级版本,3只是版本号,它在CSS2.1 的基础上增加了很多强大的新功能,目前主浏览器 Ch
2023-06-15

php的作用有哪些

这篇文章主要讲解了“php的作用有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“php的作用有哪些”吧!php即“超文本预处理器”,是在服务器端执行的脚本语言,用于web开发;php的作
2023-07-05

css的作用有哪些

这篇文章给大家分享的是有关css的作用有哪些的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。css的作用是:1、主要用来设计网页的样式,美化网页;2、能够对网页中元素位置的排版进行像素级精确控制,支持几乎所有的字体
2023-06-06

编程热搜

目录