vue开发父子组件通信的一个小坑

1,351 阅读6分钟

记录一下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 {
      agenull
    };
  }
};
</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 {
      agenull
    };
  }
};
</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: {
      deeptrue,
      handler(nv, ov) {
        this.age = nv.age;
      }
    }
  },
  data() {
    return {
      agenull
    };
  }
};
</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 {
      updatefalse,
      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 {
      agenull
    }
  },

  mounted () {
    this.age = this.message.age
  }
}
</script>

看一下效果 完美解决...

本文使用 mdnice 排版