前言
接着之前的出现的那个问题 特定的操作之后响应式对象不“响应“了(一)
上一篇文章主要是 说的是 该问题的调试方式, 是在 对于代码不太熟悉的情况下, 尽快定位 问题, 以及处理方式的一些 手法
本文这里 说一下 这个问题的具体的情况, 这里 复现出问题, 然后 完整的 查看一下 这一系列的细节 到底是怎么回事
一个基本的原则是 : 不要更新 props 传入的数据
主要的问题是 关于 父子组件的交互, 这里是 反例
测试用例
父组件 用例如下, 这里两个用例 主要是 父子组件 进行数据的交互, 然后 在父组件模板不同的情况下 竟然还有一些 奇怪的现象, 查看下面 备注 case1, 2, 3
<template>
<div class="testParent">
<!-- case1. 注释掉 el-card, el-button 子节点 click 正常, emit 之后 子节点 click 正常 -->
<!-- case2. 注释掉 el-card, 父子节点 click 正常, emit 之后 子节点 click 正常, 父节点 click 不正常 -->
<!-- case3. 不注释任何, 父子节点 click 正常, emit 之后 子节点 click 不正常, 父节点 click 不正常 -->
<el-card v-for="item in list" :key="item.idx" >
{{text}} - {{item.value}}
</el-card>
<el-button @click="handleClick">HelloParent点击+1</el-button>
<HelloWorld v-for="item in list" :key="item.idx" :counter="item" @emitToParent="handleEmitToParent"></HelloWorld>
</div>
</template>
<script>
export default {
name: "HelloParent",
components: {
HelloWorld: () => import('./HelloWorld'),
},
data() {
return {
text: 'HelloParent',
list: [
{idx : 0, value : 2}
]
}
},
computed: {},
created() {
},
methods: {
handleClick: function (e) {
for(let i in this.list) {
this.list[i].value += 1
}
},
handleEmitToParent: function (clonedCounter) {
// 在实际出现问题的情况中, 这里影响到了 父组件 的 counter
this.list[clonedCounter.idx] = clonedCounter
}
},
watch: {
// list: {
// deep: true,
// handler(newValue, oldValue) {
// console.log(oldValue + " -> " + newValue);
// }
// }
}
}
</script>
<style scoped lang="scss">
.testParent {
padding-left: 30px;
}
</style>
子组件用例如下
<template>
<div class="test">
<el-card>
{{text}} - {{counter.value}}
</el-card>
<el-button @click="handleClick">HelloWorld点击+1</el-button>
<el-button @click="handleObserveClick">HelloWorld点击Emit</el-button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
components: {},
props: {
counter: {
type: Object,
default: () => {
}
}
},
data() {
return {
text: 'HelloWorld'
}
},
computed: {},
created() {
},
methods: {
handleClick: function (e) {
this.counter.value += 1
},
handleObserveClick: function (e) {
let clonedCounter = JSON.parse(JSON.stringify(this.counter))
this.$emit("emitToParent", clonedCounter)
}
}
}
</script>
<style scoped lang="scss">
.test {
padding-left: 30px;
}
</style>
为什么组件中可以直接使用 this.counter
props 属性的 counter 是存放于 VueCompoennt._props
然后 初始化的时候会为 VueComponent 创建每一个 _props 属性的代理[data属性是相同的道理]
然后 比如我们在 handleClick 中访问 this.click, 是访问的是 proxyGetter, 然后在访问的是 this._props[‘counter’]
this._props[‘counter’] 本身会有 reactiveSetter, reactiveGetter 来支持响应式相关
不注释任何地方, 父子节点 click 正常, emit 之后 子节点 click 不正常, 父节点 click 不正常
现在操作如下, 点击 HelloWorld 的 click 按钮, 可以发现 父子组件 的内容均会响应式更新
点击 HelloParent 的 click 按钮, 可以发现 父子组件 的内容均会响应式更新
然后 点击 HelloWorld 的 emit 按钮
然后 再来点击 HelloWorld 的 click 按钮, 或者 HelloParent的 click 按钮 发现均不生效了
我们这里重点关注一下 异常情况
点击 HelloWorld 的 click 之后, 现场如下
点击 HelloParent 的 click 之后, 现场如下
调试异常情况如下
从堆栈信息可以看出的是目前断点这里是在更新渲染 HelloParent 组件, 然后遍历处理到了 HelloParent 的子节点 HelloWorld
在 prepatch 的处理中会更新 子组件 的相关信息, 我们这里关注的是 props
先看一下 目前的上下文, 子组件中 counter 的值是 5, 然后 父组件中的 props 的 counter 值为 4
另外就是 propsData 中的 counter 不是响应式对象了, 这是一个 很关键的地方
然后 这里将 子组件的 _props 更新为了父组件上下文的 props, 因此 最终渲染的结果是 4
另外就是 更新了子组件的 vm.$options.propsData
堆栈信息如下
子组件的 propsData 更新了之后情况如下, vm.counter/vm._props_counter/vm.$options.propsData.counter 已经不再是响应式对象了
这是一个很关键的地方
当然 更重要的是 vm.counter/vm._props_counter 和渲染息息相关
然后 父组件发送通知渲染子组件, 子组件渲染出来结果为 “HelloWorld - 4”
点击了 emit 之后 父组件 propsData 中的 counter 不是响应式对象了
这里 clonedCounter 是来自于 HelloWorld 中 JSON.parse(JSON.stringify(counter)) 得到的, 是一个普通的对象, 无响应式处理相关附加信息
这里是直接使用 "list[0] = xx" 更新 list[0] 之后, 父组件的 list[0] 变成了一个 无响应式处理的对象
handleEmitToParent 处理了之后 上下文信息如下
下一次重新渲染 HelloParent 的时候, 子组件 propsData.counter 不是响应式对象了
所以再细节一点
在点击了 HelloWorld 的 emit 按钮之后, 父组件的 list[0] 已经不是响应式对象了, 此时点击 父组件的 click 按钮, 会增加 this.list[*] 相关的值, 但是不会 重新渲染 父子组件
然后 在点击 子组件的时候, HelloWorld 的 counter 才变成 不是响应式对象, 并且更新了 相关属性为 父组件的相关属性, 然后 重新渲染
在此之后 子组件 的 counter 也变成了 非响应式对象, 点击 counter 之后仅仅更新了 value 的值, 无其他 响应式操作
会有如下一些现象, 点击 HelloWorld 的 emit 按钮, 然后 之后不管怎么点击 HelloWorld/HelloParent 的 click 按钮, 无任何效果
点击 HelloWorld 的 emit 按钮, 然后点击 HelloParent 的 click按钮两下
然后 父子组件渲染为 6, 之后不管怎么点击 HelloWorld/HelloParent 的 click 按钮, 无任何效果
注释掉 el-card, 父子节点 click 正常, emit 之后 子节点 click 正常, 父节点 click 不正常
此时 我们修改 HelloParent 的代码如下, 我们去掉 el-card 的渲染
此时我们可以发现的情况是 在点击 HelloWorld 的 emit 之前, 点击 父子组件 的 click 均是没有问题的
然后 我们点击了 子组件 的click之后, 可以发现 父组件 click 不会响应式更新 子组件的显示了
然后 子组件 点击, 可以正常更新
点击父组件, 情况如下
点击子组件, 情况如下
在上面 更新 props 的地方打上断点, 发现 HelloWorld 没有被重新渲染
因此 子组件点击 还能生效的原因, 就是 HelloParent 没有重新渲染, 还没有更新 子组件的 counter, 子组件的 counter 目前还是响应式的
我们调整一下 HelloParent 的代码, 我们将 emit 操作 和 HelloParent的渲染关联起来
然后 此时 本测试用例 就和 上面 “不注释任何代码” 的测试用例的效果是一样的了
不过 差别是在于 这里是 emit 按钮操作之后, HelloWorld 中的 counter 转换为不是响应式对象了
然后 上面是 点击 HelloWorld 的 click 按钮的时候, HelloWorld 中的 counter 转换为不是响应式对象了
子组件的转换的地方关键是 HelloParent 的重新渲染
注释掉 el-card, el-button 子节点 click 正常, emit 之后 子节点 click 正常
这个就和上面的这个 用例的情况一致了, 只是说 没有 父节点的 click 按钮
无法 观察 父节点的 click 按钮的相关现象
完