$parent/$children的使用场景 -- vue组件通信系列

4,360 阅读3分钟

vue 组件的数据通信方式很多,本篇着重讲$parent/$children,神助是$broadcast/$dispatch

$parent/$children的常用场景:封装嵌套组件时,直接使用长辈或者子孙组件的方法,该方法并不改变数据,常常结合$broadcast/$dispatch使用。

什么是$parent/$children

先问一句,div标签,其父元素和子元素是谁?

这其实没有答案。

父元素和子元素是谁,取决于运行时div的位置。

试着说说,以下div的父元素和子元素。

<body>
  <div id="div1">
    <main>
      <div id="div2">
        <h1>一级标题<h1>
        <p>段落<p>
      </div>
    <main>
  </div>
</body>

div1:

  • 父元素是,body
  • 子元素是,main。注意是子元素是指直接后代。

div2:

  • 父元素是,main
  • 子元素是,h1和p。注意是子元素是指直接后代。

总结下:

  • 元素本身没有父元素和子元素,而是在运行时,每个元素实例的位置,决定其父元素和子元素
  • 每个元素实例有且只有一个父元素(顶级元素实例没有哈)
  • 但每个元素实例可能没有子元素,可能有一个,也可能有多个,所以一般children是数组,没有的时候是空数组
  • js 运行时,若添加或者删除元素实例,那些发生位置变化的元素实例们,父元素和子元素也会发生变化。

正文来了!!!组件也是一样滴!!!

因为本来组件就是模仿元素的嘛!!!

把上面的元素成组件即可!

  • 组件本身没有父组件和子组件,而是在运行时,每个组件实例的位置,决定其父组件和子组件
  • 每个组件实例有且只有一个父组件(顶级组件实例没有哈)
  • 但每个组件实例可能没有子组件,可能有一个,也可能有多个,所以一般children是数组,没有的时候是空数组
  • js 运行时,若添加或者删除组件实例,那些发生位置变化的组件实例们,父组件和子组件也会发生变化。

其实属性和事件这种机制,也可以类比元素理解,当然这是后话。

举例说明$parent/$children

写一个页面组件,里面放些组件,打印下$parent/$children

parent

<template lang="pug">
//- 页面组件
div
  list-item
  list-item
</template>
<script>
import ListItem from "@/components/ListItem";
export default {
  name: "List",
  components: { ListItem },
  mounted() {
    console.log("页面的$parent", this.$parent);
    console.log("页面的$children", this.$children);
  }
};
</script>

其实还能看到,渲染的时候先子组件的mounted,然后再是自己的mounted.
要是想在子组件里获取父组件的元素之类的,必须使用nextTick

$dispatch

在使用element-ui的时候,有个el-form,大约是这么用的:

<template lang="pug">
el-form
  el-form-item
    el-input
</template>

假设el-input想要执行el-form上的方法,就会这样this.$parent.$parent.methodXx(),更多层级可能更复杂,于是$dispatch就诞生了。

// main.js
// 向上某个组件,派发事件
Vue.prototype.$dispatch = function(eventName, componentName, ...args) {
  let parent = this.$parent;
  while (parent) {
    // 只有是特定组件,才会触发事件。而不会一直往上,一直触发
    const isSpecialComponent = parent.$options.name === componentName;
    if (isSpecialComponent) {
      // 触发了,就终止循环
      parent.$emit(eventName, ...args);
      return;
    }
    parent = parent.$parent;
  }
};

这样在el-input里想要触发el-form里的方法,this.$dispatch('changeSort','el-form',{isAsc:true})

$broadcast

同理,假设el-form想要执行el-input上的方法,就会这样this.$children[0].$children[0].methodXx(),更多层级可能更复杂,于是$broadcast就诞生了,使用的时候this.$broadcast('changeValue','el-input','hello')

注意,$children 是数组,所以当只有一个子组件时,使用[0]获取。当有多个子组件时,它并不保证顺序,也不是响应式的。

// 向下通知某个组件,触发事件
Vue.prototype.$broadcast = function(eventName, componentName, ...args) {
  // 这里children是所有子组件,是子组件不是后代组件哈
  let children = this.$children;
  broadcast(children);
  // 这里注意,抽离新的方法递归,而不是递归$broadcast
  function broadcast(children) {
    for (let i = 0; i < children.length; i++) {
      let child = children[i];
      const isSpecialComponent = child.$options.name === componentName;
      if (isSpecialComponent) {
        // 触发了,就终止循环
        child.$emit(eventName, ...args);
        return;
      }
      // 没触发的话,就看下有没有子组件,接着递归
      child.$children.length &&
        child.$broadcast(eventName, componentName, ...args);
    }
  }
};

更新

感谢评论区,以上可以作为思路拓展,但官方文档不建议使用broadcast/broadcast/dispatch,而是倾向于用eventBus的模式。