前言

接着之前的出现的那个问题 特定的操作之后响应式对象不“响应“了(一) 

上一篇文章主要是 说的是 该问题的调试方式, 是在 对于代码不太熟悉的情况下, 尽快定位 问题, 以及处理方式的一些 手法

本文这里 说一下 这个问题的具体的情况, 这里 复现出问题, 然后 完整的 查看一下 这一系列的细节 到底是怎么回事

一个基本的原则是 : 不要更新 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 来支持响应式相关 

20 特定的操作之后响应式对象不“响应“了(二)_props

不注释任何地方, 父子节点 click 正常, emit 之后 子节点 click 不正常, 父节点 click 不正常

现在操作如下, 点击 HelloWorld 的 click 按钮, 可以发现 父子组件 的内容均会响应式更新

点击 HelloParent 的 click 按钮, 可以发现 父子组件 的内容均会响应式更新

然后 点击 HelloWorld 的 emit 按钮 

然后 再来点击 HelloWorld 的 click 按钮, 或者 HelloParent的 click 按钮 发现均不生效了

我们这里重点关注一下 异常情况

点击 HelloWorld 的 click 之后, 现场如下 

20 特定的操作之后响应式对象不“响应“了(二)_响应式_02

点击 HelloParent 的 click 之后, 现场如下 

20 特定的操作之后响应式对象不“响应“了(二)_vue_03

调试异常情况如下 

从堆栈信息可以看出的是目前断点这里是在更新渲染 HelloParent 组件, 然后遍历处理到了 HelloParent 的子节点 HelloWorld

在 prepatch 的处理中会更新 子组件 的相关信息, 我们这里关注的是 props

先看一下 目前的上下文, 子组件中 counter 的值是 5, 然后 父组件中的 props 的 counter 值为 4

另外就是 propsData 中的 counter 不是响应式对象了, 这是一个 很关键的地方

然后 这里将 子组件的 _props 更新为了父组件上下文的 props, 因此 最终渲染的结果是 4

另外就是 更新了子组件的 vm.$options.propsData

20 特定的操作之后响应式对象不“响应“了(二)_vue_04

堆栈信息如下 

20 特定的操作之后响应式对象不“响应“了(二)_响应式_05

子组件的 propsData 更新了之后情况如下, vm.counter/vm._props_counter/vm.$options.propsData.counter 已经不再是响应式对象了 

这是一个很关键的地方 

当然 更重要的是 vm.counter/vm._props_counter 和渲染息息相关 

20 特定的操作之后响应式对象不“响应“了(二)_子节点_06

然后 父组件发送通知渲染子组件, 子组件渲染出来结果为 “HelloWorld - 4”

20 特定的操作之后响应式对象不“响应“了(二)_ref_07

点击了 emit 之后 父组件 propsData 中的 counter 不是响应式对象了 

这里 clonedCounter 是来自于 HelloWorld 中 JSON.parse(JSON.stringify(counter)) 得到的, 是一个普通的对象, 无响应式处理相关附加信息 

这里是直接使用 "list[0] = xx" 更新 list[0] 之后, 父组件的 list[0] 变成了一个 无响应式处理的对象

20 特定的操作之后响应式对象不“响应“了(二)_vue_08

handleEmitToParent 处理了之后 上下文信息如下

20 特定的操作之后响应式对象不“响应“了(二)_子节点_09

下一次重新渲染 HelloParent 的时候, 子组件 propsData.counter 不是响应式对象了

20 特定的操作之后响应式对象不“响应“了(二)_ref_10

所以再细节一点

在点击了 HelloWorld 的 emit 按钮之后, 父组件的 list[0] 已经不是响应式对象了, 此时点击 父组件的 click 按钮, 会增加 this.list[*] 相关的值, 但是不会 重新渲染 父子组件

然后 在点击 子组件的时候, HelloWorld 的 counter 才变成 不是响应式对象, 并且更新了 相关属性为 父组件的相关属性, 然后 重新渲染

在此之后 子组件 的 counter 也变成了 非响应式对象, 点击 counter 之后仅仅更新了 value 的值, 无其他 响应式操作  

会有如下一些现象, 点击 HelloWorld 的 emit 按钮, 然后 之后不管怎么点击 HelloWorld/HelloParent 的 click 按钮, 无任何效果 

 

20 特定的操作之后响应式对象不“响应“了(二)_子节点_11

点击 HelloWorld 的 emit 按钮, 然后点击 HelloParent 的 click按钮两下

然后 父子组件渲染为 6, 之后不管怎么点击 HelloWorld/HelloParent 的 click 按钮, 无任何效果 

20 特定的操作之后响应式对象不“响应“了(二)_props_12

注释掉 el-card, 父子节点 click 正常, emit 之后 子节点 click 正常, 父节点 click 不正常 

此时 我们修改 HelloParent 的代码如下, 我们去掉 el-card 的渲染 

20 特定的操作之后响应式对象不“响应“了(二)_响应式_13

此时我们可以发现的情况是 在点击 HelloWorld 的 emit 之前, 点击 父子组件 的 click 均是没有问题的 

20 特定的操作之后响应式对象不“响应“了(二)_vue_14

然后 我们点击了 子组件 的click之后, 可以发现 父组件 click 不会响应式更新 子组件的显示了

然后 子组件 点击, 可以正常更新 

点击父组件, 情况如下 

20 特定的操作之后响应式对象不“响应“了(二)_props_15

点击子组件, 情况如下

20 特定的操作之后响应式对象不“响应“了(二)_vue_16

在上面 更新 props 的地方打上断点, 发现 HelloWorld 没有被重新渲染 

因此 子组件点击 还能生效的原因, 就是 HelloParent 没有重新渲染, 还没有更新 子组件的 counter, 子组件的 counter 目前还是响应式的 

20 特定的操作之后响应式对象不“响应“了(二)_子节点_17

我们调整一下 HelloParent 的代码, 我们将 emit 操作 和 HelloParent的渲染关联起来 

然后 此时 本测试用例 就和 上面 “不注释任何代码” 的测试用例的效果是一样的了 

不过 差别是在于 这里是 emit 按钮操作之后, HelloWorld 中的 counter 转换为不是响应式对象了 

然后 上面是 点击 HelloWorld 的 click 按钮的时候, HelloWorld 中的 counter 转换为不是响应式对象了 

子组件的转换的地方关键是 HelloParent 的重新渲染 

20 特定的操作之后响应式对象不“响应“了(二)_响应式_18

注释掉 el-card, el-button 子节点 click 正常, emit 之后 子节点 click 正常

这个就和上面的这个 用例的情况一致了, 只是说 没有 父节点的 click 按钮 

无法 观察 父节点的 click 按钮的相关现象 

完