记录一下vue开发父子组件通信的一个小坑
场景:子组件使用echart展示图表,所需options由父组件通过prop传入,父组件中的options初始值为空,在mounted钩子函数中发起http请求获取数据然后更新options,结果子组件无法正确显示图表,经过一番查找解决了问题,通过本文做个记录,也希望能帮到以后遇到相同问题的小伙伴,示例采用demo演示
父组件
<template>
<div id="app">
<child :message="message"></child>
</div>
</template>
<script>
import child from "./components/child";
export default {
name: "app",
components: {child },
data() {
return {
message:{}
};
},
mounted(){
// 模拟异步请求
setTimeout( () => this.message.age = 18,2000)
}
};
</script>
子组件
<template>
<div>{{age}}</div>
</template>
<script type='text/ecmascript-6'>
export default {
props: ["message"],
mounted(){
// 模拟echart的初始化操作
this.age = this.message.age
},
data() {
return {
age: null
};
}
};
</script>
实际效果如图

其实这里子组件是拿到了更新后的值,如果template中的是{{message.age}}是可以显示出18的,但是项目中是传的options,子组件有个echart的setOptions操作,并不是直接将prop的数据展示,所以demo写成这样。这里是可以分析出问题所在的,子组件mounted时父组件传来的值message为空对象,this.message.age就是underfined,所以页面显示为空,2秒后父组件的值更新,子组件可以拿到新的值,但mounted钩子函数不再触发,所以age仍为underfined,在网上查了下,通常的解决办法是使用watch来监听prop。改进后代码如下
子组件
<template>
<div>{{age}}</div>
</template>
<script type='text/ecmascript-6'>
export default {
props: ["message"],
mounted() {
this.age = this.message.age;
},
watch: {
message(nv,ov){
this.age = nv.age
}
},
data() {
return {
age: null
};
}
};
</script>
看到网上的教程基本到这就结束了,子组件都可以正确渲染,but...我这页面还是老样子,依旧一片空白,查看下调试工具,还是这样:

??这是什么鬼,debug一下发现watch里的代码根本不会执行,可message明明变了啊,于是又在网络中遨游了一番,然后看到了这么一段话。

重点
Vue在实例化的时候会给data中的每个属性加上getter setter以实现响应式更新,但是新增的属性无法实现响应式更新,这里我们的父组件中message初始值为{},2秒中后设置他的age为18,因为age是新增的属性,实例初始化的时候并没有给age加上getter和setter所以watch失败。这里盗一张官网的图

讲解上图,其实上图就是一个vue Object.defineProperty() 双向数据绑定原理, 第一次渲染的时候 父组件的data中没有定义age,等2秒以后才创建了aage, 那么子组件中在mounted 获取 this.message.age 就是undefined, 又因为age是新增的属性,实例初始化的时候并没有给age加上getter和setter,所以导致上图中的getter和setter 中的data 没有监听到, 就不会重新渲染模板
修改代码,在父组件中message的初始值中添加age
Object.defineProperty() 代码实现

好了接下来继续... 父组件
<template>
<div id="app">
<child :message="message"></child>
</div>
</template>
<script>
import child from "./components/child";
export default {
name: "app",
components: {child },
data() {
return {
message:{age:null}//初始化age为null
};
},
mounted(){
// 模拟异步请求
setTimeout( () => this.message.age = 18,2000)
}
};
</script>
嗯,应该没问题了吧,打开页面,额。。。还是熟悉的页面, 数据还是没有显示出来

冷静,再分析一波。子组件watch了message,但是我们在异步代码中执行了this.message.age = 18而不是类似this.message = xxx这种操作,虽然message这个对象确实发生了改变,但是却无法触发setter,watch也就不起作用,查了下官网,发现了这个配置:deep 深度监听,
因为Object.defineProperty() 双向数据绑定data的值只能浅监听到第一层数据, 不会深度监听,而message中有age, 所以需要用到deep 深度监听

修改代码如下:
<template>
<div>{{age}}</div>
</template>
<script type='text/ecmascript-6'>
export default {
props: ["message"],
mounted() {
this.age = this.message.age;
},
watch: {
message: {
deep: true,
handler(nv, ov) {
this.age = nv.age;
}
}
},
data() {
return {
age: null
};
}
};
</script>
再次打开页面
完美 ...
方法二, 还有可以使用 this.$nextTick 来解决... ,不懂的小伙伴可以查看一下资料this.$nextTick的使用
修改代码 父组件
<template>
<div id="app">
<child v-if="update" :message="message"></child>
</div>
</template>
<script>
import child from './components/child'
export default {
name: 'App',
components: { child },
data () {
return {
update: false,
message: { }
}
},
mounted () {
// 模拟异步请求
setTimeout(() => {
this.update = false
this.$nextTick(() => {
this.update = true
this.message.age = 18
})
}, 2000)
}
}
</script>
子组件
<template>
<div>{{ age }}</div>
</template>
<script type='text/ecmascript-6'>
export default {
props: ['message'],
data () {
return {
age: null
}
},
mounted () {
this.age = this.message.age
}
}
</script>
看一下效果
完美解决...
本文使用 mdnice 排版