组件内如何获取插槽组件传入的数据,以及如何更新子组件

44 阅读3分钟

问题场景

当使用组件的时候,我们希望我们开发的组件parent,能获取插槽内组件child的数据,比如label... 可能有开发者认为直接把child的数据在传入的时候给一份一样的给parent的props就好了,但是这样开发的时候心智负担就比较大了。因为你需要手动维护额外的变量,以便你的父组件能够正确渲染。我们看一个例子以便明确这个问题。

    <template>
    // 如果能让parent组件获取插槽内的组件的label,那么可以这么写
        <parent>
            <child label="one"/>
            <child label="two"/>
        </parent>
    // 否则你只能通过额外的变量,比如计算属性childLabels来控制父组件的渲染。
        <parent :labels="childLabels">
            <child label="one"/>
            <child label="two"/>
        </parent>
    </template>

思路:

在实现parent组件的时候,通过方法假如叫(calcChilds)来访问插槽里的组件实例,以下是需要注意的几个问题。

  1. 访问时机:第一次渲染的时候,应该在mounted的阶段去访问,才能获取到插槽内的组件的实例。
  2. 判断条件:vnode.tag && vnode.componentOptions && vnode.componentOptions.Ctor.options.name
    1. vnode.tag获取标签名字的这种方式判断不稳定,主要是用来判断空字符,空字符会插在两个独立的标签之间,比如和h1和h2之间就会有一个空字符,当然应该是要有回车才有的。
    2. componentOptions是用于判断是否是一个组件,如果插槽内不是组件,componentOptions是获取不到的。
    3. vnode.componentOptions.Ctor.options.name我想应该不用解释,只是你在编写组件的时候就定义好的

这样就能获取你想要的组件对应的vnode了,紧接着从这个vnode里获取实例componentInstance。就可以获取组件实例,从而获取数据。

同步更新:

尽管这样能解决获取插槽组件里的数据问题,但是child组件数据更新时,parent组件内部却无法获取到最新数据。

具体问题:如果你有一个calcChilds方法在parent组件中,用于获取child组件的数据。但是现在页面上的child组件发生变化,parent组件中的这个方法需要再一次调用。

解决方法:

  1. 可以通过在child组件的updated生命周期中抛出一个事件,由于是插槽内的组件跟parent组件的通信,所以无法通过平常的emit和v-on来做。而是需要通过$on以及$parent.$emit来进行处理。也就是在child组件中的updated方法中,通过this.$parent.$emit('child-update')触发事件,$parent获取到的就是parent实例,然后在父组件的created生命周期方法中,监听这个事件,this.$on('child-update',this.caclChilds.bind(null)。
  2. 当然child组件在updated周期才给出事件还是不够的,因为如果在第一次加载过程中,child组件渲染完毕,parent组件紧接着渲染完成,但是都没有发生变更的话,那此时也获取不到child组件中的数据,所以需要在parent组件的mounted中去调用caclChilds方法
  3. 这样看似完善了许多,但是还是不够。因为如果你是在使用的地方,通过v-if切换child的渲染和销毁,child是无法发出updated事件的,同时parent组件也无法将child已经销毁的状态同步到parent内部。所以此时我们仍需要在parent组件的updated生命周期方法中去调用caclChilds方法

例子

千言万语,不如一个例子

MyParent组件如下:

<template>
  <div>
    <p>
      child Count:{{childs.length}}
    </p>
    <p>
      labels:{{this.labels.join()}}
    </p>
    <slot></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      childs: [],
      labels: [],
    };
  },
  methods: {
    setLabels(label) {
      this.labels.push(label);
    },
    // 更新childs,并且从child实例中获取label
    calcChilds(isForceUpdate = false) {
      const slots = this.$slots.default;
      if (slots) {
        const childsSlots = slots.filter(
          (vnode) =>
            vnode.tag &&
            vnode.componentOptions &&
            vnode.componentOptions.Ctor.options.name === "MyChild"
        );
        const childs = childsSlots.map(
          ({ componentInstance }) => componentInstance
        );

        const Changed = !(
          childs.length === this.childs.length &&
          childs.every((child, index) => child === this.childs[index])
        );
        
// 视图上的更新会触发这个方法,试想一下,调用这个方法之后会往labels中插入child中的label属性,从而更新视图。
接着这个方法再一次调用,又插入child中的label属性,接着又触发更新。如果没有以下这个判断,那么会导致无限循环。
        if (isForceUpdate || Changed) {
          this.childs = childs;
          this.setLabels(childs.map((item) => item.label));
        }
      } else if (this.childs.length === 0) {
        this.childs = [];
      }
    },
  },
  created() {
    this.$on("child-update", this.calcChilds.bind(null, true));
  },
  mounted() {
    this.calcChilds();
  },
  updated() {
    this.calcChilds();
  },
};
</script>

Mychild组件如下:

<template>
  <div>
    {{label}}
  </div>
</template>

<script>
export default {
  name: "MyChild",
  componentName: "MyChild",
  props: {
    label: {
      type: String,
      default: "",
    },
  },
  updated() {
    this.$parent.$emit("child-update");
  },
};
</script>

<style lang="scss" scoped>
</style>

最后是使用的App组件

<template>
  <div id="app">
    <img alt="Vue logo"
         src="./assets/logo.png">
    <MyParent>
      <MyChild label="firstLabel"></MyChild>
      <MyChild label="secondLabel"></MyChild>
    </MyParent>
  </div>
</template>

<script>
import MyParent from "./components/MyParent.vue";
import MyChild from "./components/MyChild.vue";

export default {
  name: "App",
  components: {
    MyParent,
    MyChild,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>