Vue gettser setter 解剖 v2.5.17

背景(可略过)

项目中的一个字段改变的问题,在vuex、props中值都已经改变了,但是组件却没有更新渲染,听同事说是二次透传的问题,不过写了个demo试了下,发现并不是这个原因造成的。于是开始了漫长的源码研究之旅。或许是我的效率太低下,也是短短续续,研究了挺长时间的。现在看来是比较明显的bug了,但是这次也算是实实在在debug了一次vue源码,记录一下。

demo

最好的方法莫过于带着问题看原理了,这里用3种方式改变了countObj的值:

  1. test1()是最普通的方式直接赋值
  2. test2()、test4()是为了测试新增属性的响应效果
  3. test3()是还原的是本次遇到的问题:其实也是重置对象,增加属性了

但是demo里面展示的结果表明,test2()虽然新增了属性,但是可以响应更新视图;而test3()将新增属性的执行放到$nextTick中就不行了。这个也很令人疑惑。这是本篇解剖的重点,如果下面这个gif的结果都和你预测的相同,那么……大神!如有分析不对的地方,敬请指正!!

Vue gettser setter 解剖 v2.5.17_第1张图片


/* js */
Vue.component('Test1', {
  props: ['countObj', 'test1', 'test2', 'test3', 'test4'],
  template: `
    
` }) Vue.component('Test2', { props: ['countObj'], template: `

count: {{countObj.count + ''}}

test: {{countObj.test + ''}}

` }) new Vue({ el: '#app', data: function () { return { countObj: { count: 1 } } }, methods: { test1(){ this.countObj.count += 1 }, test2(){ var oldVal = this.countObj.count this.countObj = {} this.countObj.count = oldVal + 1 }, test3(){ var oldVal = this.countObj.count this.countObj = {} this.$nextTick(() => { this.countObj.count = oldVal + 1 }) }, test4(){ this.countObj.test = this.countObj.count }, } })

解剖方法

看源码固然直击根源,但是多个模块间复杂的调用关系对于源码新手来说恐怕是剪不断理还乱的。这里直接采取由现象找原因的方法,直接debug定位究竟哪个方法更新了视图?漫长的追本溯源过程此处不表,下面直接给出响应式的几个重要方法,便于小伙伴们之后定位问题。

  1. defineReactive
    Object.defineProperty(…) 是响应式的本源,尤其是 reactiveSetter(…) ,会调用 notify(…),进而引发一系列的更新行为
  2. nextTick
    下一次 eventLoop 进行更新操作
  3. updateComponent
    更新行为的开始,会重新计算 props,赋值会触发 reactiveSetter(…)

解剖

Vue 官方文档已明确说明:

Vue 不能检测对象属性的添加或删除

所以第一反应 test2() 中 count 的更改应该是不会渲染到视图中的,但是结果却……啪啪打脸。Why?我们在 reactiveSetter 中 打个断点:(条件断点可以从茫茫堆栈中拯救你!)

这样就已经很明了了:

  1. this.countObj = {} 会触发 reactiveSetter,等待 nextTick updateComponent
  2. 接着执行 this.countObj.count = oldVal + 1 因为 macroTimerFunc 是下一个 eventLoop 的,所以此时 count 的值已经改变
  3. 下一个 eventLoop,macroTimerFunc 触发 updateComponent,进而 updateChildComponent 时调用 props[key] = validateProp(key, propOptions, propsData, vm)
  4. 触发 Test1.countObj 和 Test2.countObj 的 reactiveSetter,进而更新视图

这也解释了为什么执行 test3() 视图不会更新,因为:

  1. 同上1
  2. 执行 this.$nextTick(...) ,$nextTick 的回调 push 进 callbacks,在 flushSchedulerQueue 之后;并且 pending 为 true,所以不会新增一个 macroTimerFunc
  3. 同上3
  4. 同上4
  5. 执行 this.countObj.count = oldVal + 1,所以不会再更新视图了

而执行过 test2() 再执行 test1() 视图不更新,是因为:

  1. 此时 APP.countObj.count 已经没有 setter 了,所以虽然 count 值已经改变,但不会 update
  2. 再次执行 test2(),因为上一次 test1() count 已经改变,只不过视图没有体现,所以更新后会在已改变的基础上 +1

其实 test3() 这样单独看很傻,谁会特意去重置 data 啊?但是在实际项目中,和 vuex、异步请求结合到一起时,还是容易踩坑的。

那么聪明的你一定也明白了为什么 test4() 不更新视图,但下次执行 test1() 时却置后 1 了吧。
【提示:test赋值是 Number 类型】
【再提示:还不明白也没关系啊,debug用起来!】

彩蛋

附一个Vue调用过程的流程图,左侧是给属性赋值到更新Dom的流程,右侧是初始化数据的流程。【关注点不同,不代表全部调用流程】
Vue gettser setter 解剖 v2.5.17_第2张图片

你可能感兴趣的:(Vue,源码学习)