【业务踩坑】当循环子组件watch监听props...

2,141 阅读2分钟

今天在开发Vue2项目时,遇到了一个纠结点,就是子组件监听父组件传过来的props:

一般普通的子组件监听,如下代码:

<!-- 父组件 -->
<template>
  <child :info="msg"></child>
  <button @click="change">改变msg值</button>
</template>

<script>
export default {
  return {
    data() {
      msg: '123'
    }
  },
  methods: {
    change() {
      this.msg = "456"
    }
  }
};
</script>
<!-- 子组件child -->
<script>
export default {
  props: {
    info: {
      type: String
    }
  },
  watch: {
    info(newVal, oldVal) {
      console.log("newVal>>>>>>>", newVal)
      console.log("oldVal>>>>>>>", oldVal)
    }
  }
};
</script>

这种很普通的监听父组件传过来的值,没什么好说的,初始化时可以监听到,即使后来父组件修改了值,在子组件中也可以立即监听到。

虽然props传过来的只是基本类型的值,但是换成对象,也不过就是加上deep:true属性,照样还是能监听到。

但是我今天写项目遇到的是,子组件是循环遍历的,并且数据类型也变成了数组,即:

<!-- 父组件 -->
<template>
  <div v-for="item in obj">
    <child :info="item.arr"></child>
    <button @click="change(item)">改变单个子组件的msg值</button>
  </div>
</template>

<script>
export default {
  return {
    data() {
      obj: [
        {
            arr: ['1','2', '3']
      	},{
            arr: ['4','5', '6']
      	},{
            arr: ['7','8', '8']
      	}]
    }
  },
  methods: {
    change(item) {
      item.arr = ['0', '0', '0']
    }
  }
};
</script>

因为要求是每个子组件显示的内容不一样,所以不能在父组件的data里定义一个变量传给子组件,这会导致所有的子组件都会共用这个变量,导致显示的内容都是一样的,这肯定不行,所以就传了各自的item值,来保证遍历的每一个子组件显示的内容都不一样。

但是这样就导致了后续我在父组件中修改了某个子组件的值,子组件会监听不到!

<!-- 子组件child -->
<script>
export default {
  props: {
    info: {
      type: Array
    }
  },
  watch: {
    info(newVal, oldVal) {
      console.log("newVal>>>>>>>", newVal) // 初始化的时候会监听到,后续父组件修改值不会变
      console.log("oldVal>>>>>>>", oldVal)
    }
  }
};
</script>

一开始我也怀疑是数据类型变了,变成了数组,所以要用deep监听,可试了还是没用:

props: {
  info: {
    type: Array
  }
},
watch: {
  info: {
    handler(newVal, oldVal) {
    	console.log("newVal>>>>>>>", newVal) // 初始化的时候会监听到,后续父组件修改值不会变
    	console.log("oldVal>>>>>>>", oldVal)
  	},
     deep: true
}

因为上面代码是简化版,所以有的读者应该已经知道问题在哪了,我当时是代码太多一时给绕进去了,还一度怀疑是Vue2的defineProperty辣鸡!哈哈

归其根本原因,是我父组件里修改的item值不是响应式的! 也就是说,不管我后来怎么改item的值,都没有触发外面真正响应式数据obj的更新!也就是父组件的template模板根本就一点反应没有(没有更新),那模板里的子组件从哪获取新值?对吧

子组件在初始化的时候,在mounted生命周期过后,页面的watch便不会再执行了,除非触发到父组件的页面更新,再来更新子组件!

所以知道原因后,代码就主要来解决如何更新父组件页面刷新!

大概有三个方案:

第一个是利用this.$set()

this.$set(obj, index, val) // 修改哪条就更新哪条,同时会触发页面更新

第二个最简单直接:forceUpdate

change(item) {
   item.arr = ['0', '0', '0']
   this.$forceUpdate(); // 修改完值后,强制重新渲染视图,简单粗暴
}

第三个比较优雅:key

<template>
  <component-to-re-render :key="componentKey" />
</template>

export default {
  data() {
    return {
      componentKey: 0,
    };
  },
  methods: {
    forceRerender() {
      this.componentKey += 1; 
    }
  }
}

key发生更改时,vue会删除旧组件并创建新组件,它将重新初始化自身并“重置”其状态