上周五(3.27)做公司项目时,遇到一个有意思的问题,虽然解决很好解决,但是今天上午(3.30)才把整个流程梳理通畅,所以记录下来,应该是个有意思的知识点(PS:其实不是我自己解决的,全程是我请教我们组的另一位大佬帮我答惑解疑的,毕竟,我是个渣渣…我又没看过源码也看不懂~)。
问题来源于我在created生命周期中调用接口获取数据然后赋值给data中某个空对象的某个属性。这整个对象被传给子组件使用。但是我发现值都不对劲。
大致是这样的:
父组件:
父组件值定义:
子组件:
出来的结果是除了todoData.unread有数据之外,其他的全为0。
于是我就想不通了,当然,本着能用就行/数据出来就好的原则,我请教了我们组的前端大佬!
大佬给出几种解决方案:
1. 在data中定义数据的时候,就把几个属性一起声明了;
2. 我赋值的时候是直接使用“**=**”赋值的,大佬建议可以使用**$set**赋值;
试了一下,果然有效!本来想就这样不管了的,实在不知道该划什么水了+心里存了那么一丢丢好奇心,最重要的本人的求知欲是很强的(手动假笑脸),我开启了“十万个为什么”模式!
“为什么???(为什么总是我写什么都要错,而大佬写的感觉和我写的差不多,为什么就大佬的对我不对呢?我!不!!服!!!)”
然后就跟大佬探讨起来(全程舔着脸请教。其实是经过几轮错误探讨的,前几轮都有我认为有不对劲的地方,然后大佬去看源码又找问题,我提问题 – 大佬看源码解决问题…,一直到我觉得这个解释天衣无缝了,可以了,我没有疑问了,上午才算把这个事情解决完,大佬应该很烦我了吧,怎么找了这么个喜欢找茬的)
我:数据通过接口拿到放进一个对象中 在页面上传给TodoList子组件 子组件显示数据
我:我能想到的唯一可能就是请求是异步,需要时间。在此之前肯定是传给了子组件,然后因为只是更改了对象的值 导致数据没有同步更新 但是。。。。。这也不对啊 对象的值更改了子组件的值是会更改的啊
大佬:父组件没有触发视图更新,函数式组件就不会更新
我:如何才能达到触发试图更新的条件
大佬:不用 functional 就行了
我:不用也不行啊!我就是从不用functional转成使用的(各位亲,functional我都不怎么了解,也是看到这位大佬用我才跟着用的,但是想知道用functional的话可以用methods吗?我怎么都使用不了methods)
大佬:父组件 todoData 在哪声明的
我:data,赋值在created
大佬:里面的三个属性要写上(意思是data中定义todoData的时候就要声明三个属性了)
我:....(尝试之后)....emmm........原理是什么?
大佬给发了一篇vue官网的文章来(深入响应式原理那里,说实话,为什么每次我在官网上什么都找不到,而大佬总是给我惊喜?)
我:(看了之后)那 按照我的理解 我使用$nextTick不是应该也没问题吗?
我:还是用nextTick只是数据更新了 视图层并不会更新?
(我这里理解不正确,囫囵吞枣,大家别学我)
大佬:nextTick 类似于 settimeout ,和更新没有任何关系
大佬:归根结底在于 Object.defineProperty 的局限性,
大佬:只有赋值才能触发,而不是 声明,
我:我之前在created里面也有赋值啊 难道浏览器认为我是在声明属性值?就因为第一次出现?
大佬:在 data() 里面只声明 a,没声明a.b,直接 a.b = ** , 对应 b 属性来说就是声明
大佬:不是赋值
我:对象值修改不是响应式的吗 Object.defineProperty是监听了data中的a为空数组 但是我添加a.b的时候不相当于也是触发了a的getter/setter?
我:况且就算是你说的那样那也不对劲啊 我其他的有问题 但是未读消息一样是首次声明 未读消息条数没问题!
(此处自己好像绕进去了,自个人明明知道对象修改是响应式的,但是我记得在vue中修改对象好像还真不会触发视图层更新,页面不会重新渲染)
大佬:![大佬发了俩图给我](https://img-blog.csdnimg.cn/20200330172329313.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mjk3MTkzMw==,size_16,color_FFFFFF,t_70)
大佬:![大佬发了俩图给我](https://img-blog.csdnimg.cn/20200330172347792.png)
我:(无话可说,想想又觉得好像是那么个道理,又开始找不同)那我的unread是如何正确的?
大佬:unread 在 created 中同步赋值,不像另外两个是异步延迟了,vue 在 created 钩子执行后,还会做一次检测
我:就是像普通的声明某个属性可以在data中也可以在created中一样是吧
大佬:(放大招了)详细情况看源码
我:然后 用$set改变值也是OK的 所以就算没有声明某属性 也会在$set中声明吗
我:[假笑脸]我 我这个级别还看不懂源码
大佬:$set 就是给这种情况做的补充,内部再重写 set 方法
我:好的 虽然不懂是怎么实现的 但是好歹也知道了个大概
至此,第一回合结束,虽然我有点蒙圈,毕竟我在created中也是赋值了的,怎么就第一次声明就不管用了!我在created中直接都可以定义可以直接使用的data中的某个属性,虽然不懂,但是大佬说的就算不是真的,我也找不到其他人问了。
过了一个多小时,大佬又发来一段,这时候我在做其他的,没有这个了,想周末回家重新梳理思绪。
第二阶段的讨论又展开:
大佬:刚才说错了,unread 是在 created 中同步赋值,渲染真实成 dom 是在 mounted 时,那时可以拿到 unread 的值,相反,另两个属性,渲染真实成 dom 的时候还不存在,所以页面上显示不出来。但是,这三个属性依旧是不具有可响应的,后续修改无法触发更新
我:即使使用$set也不能?
大佬:set 就是让其变成可响应的,
大佬:但不建议使用,这只是补救措施
大佬:害我翻了好久的源码
(此处严重怀疑大佬在装biu~)
我:为什么不建议使用
我:我也是在消化你说的 开始还走偏了想偏了 我还在想怎么组织成我能理解的样子 看来要一会儿了
大佬:使用 set 是命令式编程,
大佬:可以使用声明式就拒绝使用命令式
我又败下阵来,被说服了,讨论了一些其他问题,讨论完了,过了几分钟,我觉得我又发现什么不对劲了,又开始了battle,踢了个小问题:
不对啊 响应式数据改变都不会触发视图更新的话 那我在data里面声明了属性,他也还是个响应式数据啊 为什么再更改又行
可惜,大佬没回答我,这期间我一直尝试将整个过程自己写出来便于理解,也相当于把过程走一遍,可惜没走通。一晃就下班还放假了,再一晃就周一了,是的周末我没找他battle,毕竟我周末床都不可能起。
今天,周一,一大早就开始找他继续话题了。第三轮争论又开始了!这次我应该是通了吧:
我:上周的争论还没结束勒
我:你说“这三个属性依旧是不具有可响应的,后续修改无法触发更新。”这一句有疑问
我:直接赋值不行是吧 我尝试在点击按钮后直接修改它的值 是可以修改的又
大佬:很大可能是其他值引起的刷新
我:等一下 我给你上代码
....中间省略无数代码以及文件直接发过去(大佬在老家,疫情交通管制无法到现成,否则我肯定会被忽的一愣一愣的,哪里来的脑子想他的话哪里有问题)
大佬翻了翻源码(是的,就是这么任性,一言不合翻源码)
大佬: 子组件的props 自动转成响应式对象,而且是深层的,里面所有的属性都会转换(还附带截图,完全看不懂那种)
我:就是不管子组件接收的是什么值 是对象属性值或者是普通字符等 都是响应式的
大佬:对于子组件来说是这样,
我(来劲了,因为问题饶了一圈又回到最初了):那问题就来了
我:那这样的话 最开始虽然已经渲染过了 但是我created请求发送之后无论如何更改了数据啊 值应该是相对应的啊
我:难道是当时渲染 传了对象过去子组件 子组件接收对象后没有属性 又在子组件给创建了新的与父组件同名的属性 所以父组件更改就不行了?
我:就是最初的问题了
大佬表示不知所云,我都快急疯了,表达能力这么差吗?
我:我在created里面修改todoData的属性 同时创建属性和赋值
我:为什么值又都是0
我:因为不是都是响应式的数据吗 那就算created生命周期完结了修改了值也能修改啊
直接上文件给大佬了
我重申了我的疑问:todoData中的unread属性是我直接用$store赋值的,这个的值就没问题。其他的在初始声明data时不声明属性的话就不行,so,why?
大佬:子组件 breforeCre\created 晚于父组件,子组件生成的时候发现只有 unread 属性,也就只有 unread 被转换了,
我:意思就是 我在子组件中使用的todoData.unlast和todoData.unpay属性和父组件后面传过来的是不一样的?
大佬:一样,但是没有被转换了
我:这个转换怎么理解
大佬:就加上了 get/set
至此,争论结束,我的“十万个为什么”也问完了。
以上,我只是将我和大佬的对话记录下来,方便以后再看。大家可能看不懂。我自己会整个梳理一下流程,毕竟要将复制粘贴的东西变成自己的,觉得这个问题蛮有趣的。
接下来由啰嗦的自己把过程重新过一遍,重新梳理一遍(当然,全部是我的自认为!)。
首先,我在data中声明空对象todoData:{},在created生命周期中赋值this.todoData.unread=this.$store.state.user.msg并调用接口在.then中赋值this.todoData.unpay=xxx;this.todoData.unlast=xxx;
整个todoData被我传给子组件,子组件中直接显示(props.todoData.unread,...)
理解是这样的:
我在data中声明了todoData但没有声明其中的属性(准备在created中直接声明兼赋值一起了),但是在created中赋值的时候它是个异步!是个异步!
除了unread是同步赋值之外,其他的都暂时被跳过了。created同步执行到了mounted完成页面渲染后给人家把数据已经传过去了。相当于现在的情况是,传过去的值:
todoData:{ unread: xxx }
只有个这个玩意儿!所以在父组件声明其他属性之前子组件已经提前把属性给声明好了只是没有值而已。之前没发现是由于我写了 || 0把值给默认成0了。
这样声明的属性,虽然父组件和子组件的属性是一致的,但是属性没有被转换为props属性
(父子组件传的值--也就是通过props传的值,都可以深度修改。我理解的是在页面初次渲染-的时候子组件给每一个props里面的属性以及对象的属性等都重写了一个getter/setter方法,所以值可以深度修改,但在本次案例中,首次渲染,传过去子组件的对象中只有一个属性是被重写了setter/getter的。)
所以我再在父组件这边修改todoData.unpay和todoData.unlast的值,人家都不理我了,谁让我上车晚了,虽然座位是我的,但是我不在车上,也不能在车上到处蹦跶了。。。。
我也测试过在created的异步请求完成之后声明一个普通的属性(非对象,因为对象直接点操作一个不存在的属性是不会报错的),结果果然在页面渲染时就报错了,貌似错误信息是传的那个属性is not define。
以上,证明大佬说的流程还是没错的。我也想不出来哪里还有问题了
至于响应式数据本来修改就不会触发页面更新的问题,大佬说了,在props里面会,具体原理是啥我也不知道,咱也不敢问,问就是看源码。
罗里吧嗦了大半天,写完这篇文章都已经不是文中的“今天”了。所以,我其实还是有那么一丢丢不知所云。大佬教正确了,我消化了那就是我的了。虽然可能哪天回来看会觉得这是什么垃圾看不懂,哈哈哈哈,希望你们别被我带偏,也别看不懂了。
感觉挺简单一过程,就是被我妖魔化,然后啰嗦了一堆。
话说,这好像也暴露了我没理解到vue生命周期具体执行顺序的问题。。。。。
至于最后,我还是用了在data中事先声明对象的每一个属性的方式解决。毕竟大佬让我能尽量用声明解决的问题就不要用$set解决。而且也省事啊。
===================================
以上的哪个结论如果哪里有什么问题,哪怕是无关此次讨论主题的一个小问题,欢迎各位大佬指出,是错误就要改正,改正了之后才会变成一个新的自己。
欢迎探讨!感谢各位大佬!