vue 组件通信的几种方式

简介

这篇文章主要来聊聊,vue 中进行组件之间通信的几种方式。以及一些细节问题和优劣

$refs

通过在组件中绑定 ref=name 属性,我们可以在 this.$refs.name 获取到当前的组件实例。如果 ref 绑定的不是一个组件而是一个 html 元素,那获取到的是 dom 对象。

通过 ref 我们可以获取到子组件的实例,通过获取实例上的属性和方法来进行组件通信

<!-- 父组件 -->
<template>
  <div>
    <!-- 若 ref 绑定的是普通 html 元素,那获取的对象就是 dom -->
    <div>
      <input type="text"
             ref="input">
      <button @click="getDom">get</button>
    </div>

    <!-- 若 ref 绑定的是组件,那获取的就是当前的组件实例 -->
    <div>
      <child ref="child" />
      <button @click="getChild">get</button>
      <button @click="setInfo">set</button>
    </div>

  </div>
</template>

<script>
import child from './child.vue'

export default {
  components: {
    child
  },
  methods: {
    getDom() {
      // 获取 dom 元素,并调用 dom 元素上的成员方法
      console.log(this.$refs.input.value)
    },
    getChild() {
      // 获取实例上的响应式属性
      console.log(this.$refs.child.info)
    },
    setInfo() {
      // 调用实例上的方法
      this.$refs.child.change()
    }
  }
}
</script>


<!-- 子组件 -->
<template>
  <input type="text"
         v-model="info">
</template>

<script>
export default {
  data() {
    return {
      info: 'child'
    }
  },
  methods: {
    change() {
      this.info = 'change'
    }
  }
}
</script>

ref 主要还是通过获取组件实例,通过直接获取和改变实例上的成员变量来实现组件通信,虽然这种做法可以很便捷的进行通信,但不利于问题的溯源,而且相对繁琐,每次都需要进行调用的编写。适用于一些比较单一组件的通信。

$parent / $children

通过 vm.$parent 我们可以获取父组件的实例对象,并可以调用父组件实例上的成员变量。
通过 vm.$children 我们可以获取所有的子组件对象,他是一个数组,数组中包含所有的子组件成员实例,我们同样可以调用所有子组件实例上的成员变量

<!-- 父组件 -->
<template>
  <div>
    <p>parent: {{ title }}</p>
    <!-- 通过 $children 获取所有的子组件实例,并调用实例上的方法 -->
    <button @click="$children[0].setParent()">child1</button>
    <button @click="$children[1].setParent()">child2</button>
    <child1 />
    <child2 />
  </div>
</template>

<script>
import child1 from './child1.vue'
import child2 from './child2.vue'
export default {
  components: {
    child1,
    child2
  },
  data() {
    return {
      title: 'ssss'
    }
  }
}
</script>

<!-- child1 -->
<template>
  <div>
    <p>
      <!-- 通过 $parent 获取父组件实例,并调用实例上的成员 -->
      child1: {{ $parent.title }}
    </p>
    <button @click="setParent">setparent</button>
  </div>
</template>

<script>
export default {
  methods: {
    setParent() {
      this.$parent.title = 'child1 set'
    }
  }
}
</script>


<!-- child2 -->
<template>
  <div>
    <p>
      child2: {{ $parent.title }}
    </p>
    <button @click="setParent">setparent</button>
  </div>
</template>

<script>
export default {
  methods: {
    setParent() {
      this.$parent.title = 'child2 set'
    }
  }
}
</script>

通过获取或修改实例上的成员的确可以实现数据的通信,但从模块化的思想来说,这种操作会增加父子组件的耦合度,导致无法比较有效的进行分离管理。容易导致意外的错误,并增加了维护难度。所以使用这类

$ref$parent/$children 的思想其实都是先获取组件的实例,然后获取实例上的成员或者调用实例上的方法来实现数据通信的

如果我们想进行 父组件-孙子组件的通信,使用上面的方法就会显得特别鸡肋、繁琐。

// 获取曾父组件
this.$parent.$parent.title

// 获取孙子组件
this.$children[0].$children[0].title

那应该怎么办呢?别着急,接着看

$provide / $inject

$provide / $inject 是一种依赖注入的方式,来优化多个层级的组件嵌套的通信的方式。

父传子

首先我们来看看父子之间如何进行数据通信

<!-- parent -->
<template>
  <div>
    <p>
      parent: {{title}}
      <child />
    </p>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  data() {
    return {
      title: 'mmmm'
    }
  },
  // 通过 provide 将需要传递的数据进行返回
  provide() {
    return {
      title: this.title,
      setParentData: this.setParentData
    }
  },
  methods: {
    setParentData() {
      this.title = 'change'
    }
  },
  components: {
    child
  }
}
</script>

<style>
</style>

<!-- child -->
<template>
  <div>
    <p>
      child: {{title}}
    </p>
    <!-- 尝试改变title 看看会不会影响父组件中 title 的值 -->
    <button @click="setData">setData</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
    }
  },
  // 子组件通过 inject 来接收父组件传递的 provide 数据,这里你可以规定接收哪些不接收哪些。比如我这里只接收 title 属性
  inject: ['title'],
  methods: {
    setData() {
      this.title = '222'
    }
  }
}
</script>

<style>
</style>

在这里插入图片描述

父组件通过 provide 将属性传递给子组件,子组件则通过 inject 来接收父组件传递的值。但是当我们在子组件中改变 title 属性的时候,实际是会报一个错误的。从上面的图我们也可以看见,当改变 title属性的时候,子组件的 title 的确被修改了,但父组件的依然是原来的值。
在这里插入图片描述
实际上 provide 的数据并不是响应式的,更像是父组件在初始化的时候给子组件拷贝了一份数据供他使用。即便我们修改父组件中的 title 属性,子组件中的 title 也不会修改

比如我们在父组件增加一个按钮来修改 title属性

<template>
  <div>
    <p>
      parent: {{title}}
    </p>
    <button @click="setParentData">change</button>
    <child />
  </div>
</template>

在这里插入图片描述
所以即便我们通过 provide/inject 传递了一个可以设置父组件 title 属性值的方法。实际上也只能改变父组件中的 title,不能实现响应式。

虽然你可能可以通过一些变通来间接的实现父子组件的数据同时更新,但这会使整个项目难以维护。并且一旦涉及多个层级的数据传递,这种数据更新会很难实现,并且可能出现更多的意外情况。

从这个层面来看,我认为 $provide/$inject 更适合多层级组件之间的数据单向传递,但如果涉及子传父的话可能就显得比较困难。

父传孙子

在子组件不定义 provide 属性的适合,子组件会将父组件的 provide 向下传递。

<!-- grandson-->
<template>
  <div>
    <p>
      grandson: {{title}}
    </p>
    <button @click="setData">setData</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
    }
  },
  inject: ['title', 'setParentData'],
  methods: {
    setData() {
      this.setParentData()
    }
  }
}
</script>

<style>
</style>

但同样的,传递的数据并不是响应式的。

冲突问题

在使用 provide/inject 的时候,你还需要注意一些命名冲突问题。

使用 provide 进行数据通信,需要避免与 props 传递的数据产生冲突。一旦 provideprops 产生冲突,props 传递的数据会被 provide/inject 的数据覆盖

<!--parent-->
<child title="0000" />

<!--child-->
<template>
  <div>
    <p>
      child: {{title}}
    </p>
    <button @click="setData">setData</button>
  </div>
</template>

<script>
export default {
  props: ['title'],
  data() {
    return {
    }
  },
  inject: ['title', 'setParentData'],
  methods: {
    setData() {
      this.title = 'chang'
      this.setParentData('change')
    }
  }
}
</script>

在这里插入图片描述
同样的,如果 provide/inject 中的属性名与组件本身 data 冲突,会优先使用 data 的属性。注意,这里是优先使用,而不是覆盖

data() {
  return {
    title: '9999'
  }
},
inject: ['title', 'setParentData'],

在这里插入图片描述
在这里插入图片描述

$attrs / $listener

$attrs

当我们在组件中加入属性时,若子组件中没有使用props接收对应的属性的时候,这些加入的属性将会存储在 $attrs 属性。但我们需要注意 classstyle 属性是由默认的作用,是没办法被接收的

<template>
  <div>
    <p>
      parent: {{ title }}
    </p>
    <child :title="title"
           name="attr"
           class="parent-class"
           style="color:red" />
  </div>
</template>

在这里插入图片描述
如果其中的某些属性定义在props中,那么会优先存储在 props 内。
在这里插入图片描述
如果你想访问这些数据,你可以在 vm.$attrs 中访问数据。但如果你试图修改 vm.$attrs 的数据是没办法实现响应式渲染的。

<template>
  <div>
    <p>child: {{$attrs.title}}</p>
    <button @click="changeData">chang</button>
  </div>
</template>

<script>
export default {
  props: ['name'],
  // 实际上 inheritAttrs 是控制是否将属性继承到组件的最外层元素上,而 attrs 是一直存在的
  // inheritAttrs: false,
  methods: {
    changeData() {
      this.$attrs.title = 'change---'
    }
  }
}
</script>

$listener

$listener 包含了绑定在子组件上的所有事件,我们可以通过访问 vm.$listener 来获取事件操作。这样我们就可以通过传递的事件来修改父组件的数据,并且这种改动是响应式的。不仅会改变父组件的状态,同时也会改变子组件的状态

<!-- 父组件 -->
<child :title="title"
           name="attr"
           class="parent-class"
           style="color:red"
           @change="change" />

<!-- 子组件 -->
<script>
export default {
  props: ['name'],
  methods: {
    changeData() {
      this.$listeners.change()
    }
  }
}
</script>

在这里插入图片描述

继承

你需要注意的是,默认情况下,父组件绑定到子组件上的属性会继承到组件的最外层元素上。
在这里插入图片描述
如果你不希望最外层元素继承,你可以定义 inheritAttrs: false。但 class 和 style 属性是默认继承的,不管 inheritAttrs 是什么

<script>
export default {
  props: ['name'],
  // 实际上 inheritAttrs 是控制是否将属性继承到组件的最外层元素上,而 $attrs 是一直存在的
  inheritAttrs: false,
}
</script>

在这里插入图片描述

父传孙

如果你希望这种属性可以传递给你元素,你可以使用 v-bind="$attrs" v-on="$listener" 这两个指令的意思就是将 $attrs $listeners 上的属性展开到某个组件上,这样就可以将父组件数据通过子组件间接传递给孙子

<template>
  <div>
    <p>child: {{$attrs.title}}</p>
    <button @click="changeData">chang</button>
    <grandson v-bind="$attrs"
              v-on="$listeners" />
  </div>
</template>

在这里插入图片描述

总结

vue 组件通信的方式还有很多,比如事件总线,vuex 等也可以。每种方式都有它的好处和缺点,没有绝对的最优选择,这主要取决于你希望如何进行数据传递。