Blog icon indicating copy to clipboard operation
Blog copied to clipboard

Vue系列之组件通信有哪几种方式

Open yuanyuanbyte opened this issue 4 years ago • 0 comments

本系列的主题是 Vue,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末

如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。

Vue 组件通信有哪几种方式

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。

Vue 组件通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

props$emit 父子组件通信

父组件向子组件传递数据是通过 props 传递的

子组件传递数据给父组件是通过 $emit 触发事件来做到的

props 示例 (使用了 v-bind 动态赋值):

// 父组件
<heading :title="title" :level="level"></heading>
export default {
  components: { Heading },
  data() {
    return {
      title: "标签。",
      level: "1",
    };
  },
}

// 子组件 heading
export default {
  props: ["level", "title"],
}

$emit 示例:

// 父组件 
<Child @getAdvice="showAdvice"/>

method () {
  showAdvice(advice) {
    console.log(advice);// 输出 do it!
  }
},    

// 子组件 Child.vue
export default {
  data() {
    return {
      advice: "do it!"
    };
  },
  mounted() {
    this.$emit("getAdvice", this.advice);
  }
}

$parent$children 访问父 / 子组件实例

注意 ❗ ❗ ❗ 在 Vue 3.x 中,$children property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用 $refs

正常情况下也不建议通过$children来访问子组件实例!

因为 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。(即通过 v-for 配合数组循环生成子组件的情况下才可以考虑使用$children来访问子组件实例)

$parent$children 访问父 / 子组件实例的 示例:

index.vue (父组件)

<template>
  <div>
    parent
    <children></children>
  </div>
</template>
<script>
import children from "./components/children.vue";
export default {
  components: { children },
  data() {
    return {
      parentKey: "value",
    };
  },
  mounted() {
    // this.$children 是一个数组
    console.log(this.$children);

    /**
     *  $children 并不保证顺序,也不是响应式的
     */
    // 调用子组件实例方法  如果你能清楚的知道子组件的顺序,可以使用下标来操作
    this.$children[0].childrenSay("yes");
  },
  methods: {
    parentSay(value) {
      console.log("parents组件说" + value);
    },
    saySelf() {
      console.log(this.parentKey);
    },
  },
};
</script>

children.vue (子组件)

<template>
  <div>children组件</div>
</template>
<script>
export default {
  data() {
    return {
      key: "value",
    };
  },
  mounted() {
    // 调用父组件方法
    this.$parent.parentSay("no");
    // 修改父组件data数据
    this.$parent.parentKey = "children dad";
    // 验证父组件data数据是否修改
    console.log(this.$parent.parentKey);
    // 再次验证父组件data数据是否修改
    this.$parent.saySelf();
  },
  methods: {
    childrenSay(value) {
      console.log("children组件说" + value);
    },
  },
};
</script>

在这里插入图片描述

$refs 访问子组件实例或子元素

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

通过 ref 这个属性为子组件赋予一个 ID 引用。例如:

<base-input ref="usernameInput"></base-input>

在定义了这个 ref 的父级组件里,你可以使用:

this.$refs.usernameInput

$refs 示例:

<template>
  <div>
    parent
    <children ref="child"></children>
  </div>
</template>

mounted() {
  // 调用 ref="child" 子组件实例的方法
  this.$refs.child.childrenSay("父组件通过refs调用了我");
  // 打印 ref="child" 子组件实例的 data
  console.log(this.$refs.child.key);

  console.log(this.$refs.child);
}

// children 组件方法:
childrenSay(value) {
  console.log("children组件说:" + value);
},

在这里插入图片描述

eventBus 事件总线 ($emit / $on)

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

eventBus 事件总线 使用步骤如下:

(1) 创建 事件总线 管理组件之间的通信

// eventBus.js

import Vue from 'vue'

const eventBus = new Vue()

export default eventBus

(2) 父组件引入两个兄弟组件

// 父组件

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

(3) 发送事件 (在firstCom组件中发送事件)

// firstCom.vue

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>
<script>
import {eventBus} from './eventBus.js' // 引入事件总线
export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      eventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

(4) 接收事件 (在secondCom组件中接受事件)

// secondCom.vue

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { eventBus} from './eventBus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    eventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

$on( event, callback ) 方法:

监听当前实例上的自定义事件。事件可以由 $emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

$attrs / $listeners 实现多级嵌套组件通信

  • 通过 this.$attrs 访问父作用域传递给组件的 属性 (获取祖先传的值),注意:不包括被子组件声明为 props 绑定的属性值,即已在 props 声明的属性值可以直接通过this 调用,不再通过$attrs传递。

  • 通过 v-bind="$attrs" 传入内部组件,可以将属性绑定继续向下传递;如果中间层组件想要添加其他属性,可继续绑定属性。但要注意的是,继续绑定的属性和 $attrs 中的属性有重复时,继续绑定的属性优先级会更高(向子组件传递从父组件或者更高层组件获取的值绑定)

  • 通过 this.$listeners 访问父作用域传递给组件的 事件监听器。当然也可以通过this.$emit()访问,两种方式对比:

    this.$listeners.log1("$listeners执行 log1方法"); this.$emit("log1", "$emit执行 log1方法");

  • attrs 属性一样,可以通过 v-on="$listeners" 传入内部组件,将事件监听器继续向下传递。如果想要添加其他事件监听器,可继续绑定事件。但要注意的是,继续绑定的事件和 $listeners 中的事件有重复时,不会被覆盖,重复绑定的事件都会被执行,会通过 事件冒泡 执行重复绑定的事件,即 从下往上 执行重复绑定的事件, (这点和属性绑定差别很大,下面会有实例讲解)

$attrs$listeners 就像两个容器,分别存储着 父组件及更高层组件 的 属性 (值绑定)事件监听器 (事件绑定),实现跨多级的嵌套组件通信

理解一下:

  • this.$attrsv-bind="$attrs" 搭配使用实现 值传递,前者是获取祖辈传递下来的值 (不包括被子组件声明为 props 绑定的属性值),后者是将 值 向下传递;

  • this.$listenersv-on="$listeners" 搭配使用实现 事件传递,前者是访问获取到的事件监听器,后者是将 事件监听器 向下传递;

  • this.$attrsthis.$listeners 也都可以 单独使用,分别用来获取 父组件及更高层组件 传递的 事件监听器。 (想要获取更高层组件就要在父组件及更上次组件声明 v-bind="$attrs"v-on="$listeners" )

通俗的理解为:子辈可以通过 $attrs$listeners 将父组件内注册的 以及祖辈传递下来的 参数 和 事件监听器 接收,并传递给孙辈。

inheritAttrs 选项

inheritAttrs 是组件的一个选项,有 truefalse 两个值。默认情况下 vue 会把父作用域的不被认作 props 的特性 绑定为普通的 HTML 特性应用在子组件的根元素上,例如:

<div id="app">
    <children-component :name="name" :age="age"></children-component>
</div>
var vm = new Vue({
    el: "#app",
    data: {
        name: "juliet",
        age: 23
    },
    components: {
        "chilren-component": {
            props: ["name"],
            template: `<div>{{ name }}</div>`
        }
    }
})

而最终渲染出来是下面这样:

<div id="app">
	<div age="23">juliet</div>
</div>

age 被当作子组件根元素上的属性了,而在某些特定场景下为我们不希望出现这种情况,那么可以在子组件中加上 inheritAtrrs = false 属性,如下所示:

var vm = new Vue({
    el: "#app",
    data: {
        name: "juliet",
        age: 23
    },
    components: {
        "chilren-component": {
            props: ["name"],
            inheritAttrs: false,
            template: `<div>{{ name }}</div>`
        }
    }
})

这样最终渲染出来是下面这样的:

<div id="app">
	<div>juliet</div>
</div>

age 不再被渲染为子组件根元素上的属性了,那要怎么获得 age 的值呢,这时就可以用 $attrs 来获取了:

var vm = new Vue({
    el: "#app",
    data: {
        name: "juliet",
        age: 23
    },
    components: {
        "chilren-component": {
            props: ["name"],
            inheritAttrs: false,
            template: `
                  <div>
                        <div>{{ name }}</div>
                        <div>{{ $attrs.age }}</div>
                  </div>
            `
        }
    }
})

最终渲染出来的是下面这样的:

<div id="app">
    <div>
        <div>juliet</div>
        <div>23</div>
    </div>
</div>

所以 $attrs 是包含了在父组件中除了注册在 props 中的其余属性的对象。它还有另外一个作用,假设现在有父子孙三个组件,如果想让父组件传值给孙组件该怎么办,常规办法就是利用 props 一级一级的往下传递,如下所示:

<div id="app">
    <children-component :name="name" :age="age"></children-component>
</div>
var vm = new Vue({
    el: "#app",
    data: {
        name: "juliet",
        age: 23
    },
    components: {
        "children-component": {
            props: ["name", "age"],
            template: `
                  <div>
                        <div>{{ name }}</div>
                        <grand-component :age="age"></grand-component>
                  </div>
            `,
            components: {
                "grand-component": {
                    props: ["age"],
                    template: `
                        <div>{{ age }}</div>
                    `
                }
            }
        }
    }
})

最终渲染出来的效果如下:

<div id="app">
    <div>
        <div>juliet</div>
        <div>23</div>
    </div>
</div>

虽然达到了最终的效果,但是子组件并没有使用到 age 的值,但由于孙组件要使用 age,所以它不得不接收父组件传过来的 age 并且将其传给孙组件。现在只有 3 层组件,如果有 5 层甚至十几层呢,得写很多多余的 propsattribute 来传递值。但是使用 $attrs 就不用这么麻烦,如下所示:

<div id="app">
    <children-component :name="name" :age="age"></children-component>
</div>
var vm = new Vue({
    el: "#app",
    data: {
        name: "juliet",
        age: 23
    },
    components: {
        "children-component": {
            props: ["name"],
            template: `
                  <div>
                        <div>{{ name }}</div>
                        <grand-component v-bind="$attrs"></grand-component>
                  </div>
            `,
            inheritAttrs: false,
            components: {
                "grand-component": {
                    template: `
                        <div>{{ $attrs.age }}</div>
                    `,
                    inheritAttrs: false
                }
            }
        }
    }
})

渲染出来的和上面的结果一模一样,但是因为 age 属性是传给孙组件的,子组件的 props 中就不需要传 age 属性了,而是直接通过 v-bind="$attrs" 传给孙组件即可。

这里需要注意的是,inheritAttrs: false 不会影响 styleclass 的绑定,也就是说在父组件对子组件的 styleclass 属性进行了设置,那么即使子组件设置了 inheritAttrs: false,这两个属性还是会合并到子组件的根元素上。

$attrs / $listeners 实现多级嵌套组件通信 示例:

test1.vue

<template>
  <div>
    <h1>我是 test1 组件</h1>
    <hr />
    <test2
      :foo="foo"
      :boo="boo"
      :coo="coo"
      :doo="doo"
      @log1="log1"
      @log2="log2"
    ></test2>
  </div>
</template>
<script>
import test2 from "./test2.vue";
export default {
  components: { test2 },
  data() {
    return {
      foo: "foo",
      boo: "boo",
      coo: "coo",
      doo: "doo",
    };
  },
  methods: {
    log1(value) {
      console.log("我是test1组件的log1方法:" + value);
    },
    log2(value) {
      console.log("我是test1组件的log2方法:" + value);
    },
  },
};
</script>

test2.vue

<template>
  <div>
    <h1>我是 test2 组件</h1>
    <p>props: {{ $props }}</p>
    <p>attrs: {{ $attrs }}</p>
    <button @click="toParent1()">触发log1方法</button>
    <hr />
    <test3 v-bind="$attrs" :coo="coo" v-on="$listeners"></test3>
  </div>
</template>
<script>
import test3 from "./test3.vue";
export default {
  components: {
    test3,
  },
  inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
  props: {
    foo: String, // foo作为props属性绑定
  },
  data() {
    return {
      coo: "test2的coo",
    };
  },
  created() {
    console.log("test2组件this.$attrs:", this.$attrs);
    console.log("test2组件this.$props:", this.$props);
    console.log("test2组件this.$listeners", this.$listeners);
  },
  methods: {
    toParent1() {
      this.$listeners.log1("test2组件 $listeners执行 log1方法");
      this.$emit("log1", "test2组件 $emit执行 log1方法");
    },
  },
};
</script>

test3.vue

<template>
  <div class="border">
    <h1>我是 test3 组件</h1>
    <p>props: {{ $props }}</p>
    <p>attrs: {{ $attrs }}</p>
    <button @click="toParent1">触发log1方法</button>
    <button @click="toParent2">触发log2方法</button>
    <hr />
    <test4 v-bind="$attrs" @log1="log1" v-on="$listeners"></test4>
  </div>
</template>
<script>
import test4 from "./test4.vue";
export default {
  components: {
    test4,
  },
  inheritAttrs: false,
  props: {
    boo: String,
  },
  created() {
    console.log("test3组件this.$attrs:", this.$attrs);
    console.log("test3组件this.$props:", this.$props);
    console.log("test3组件this.$listeners", this.$listeners);
  },
  methods: {
    toParent1() {
      this.$emit("log1", "test3组件 $emit执行 log1方法");
      this.$listeners.log1("test3组件 $listeners执行 log1方法");
    },
    toParent2() {
      this.$emit("log2", "test3组件 执行 test1组件方法");
    },
    log1(value) {
      console.log("我是test3组件的log1方法:" + value);
    },
  },
};
</script>

test4.vue

<template>
  <div class="border">
    <h1>我是 test4 组件</h1>
    <p>props: {{ $props }}</p>
    <p>attrs: {{ $attrs }}</p>
    <button @click="toParent1">触发log1方法</button>
    <button @click="toParent2">触发log2方法</button>
  </div>
</template>
<script>
export default {
  props: {
    coo: String,
    title: String,
  },
  created() {
    console.log("test4组件this.$attrs:", this.$attrs);
    console.log("test4组件this.$props:", this.$props);
    console.log("test4组件this.$listeners", this.$listeners);
  },
  methods: {
    toParent1() {
      this.$emit("log1", "test4组件 $emit执行 log1方法");
      this.$listeners.log1("test4组件 $listeners执行 log1方法");
    },
    toParent2() {
      this.$emit("log2");
    },
  },
};
</script>

页面展示效果:

在这里插入图片描述

多级嵌套组件通信 参数传递 描述:

test1 组件一共传了四个变量:foo、boo、coo、doo,两个事件:log1log2

test2 组件中 props 中接收了一个 foo。所以$attrs传的是剩下的 { "boo": "boo", "coo": "coo", "doo": "doo"}

test2 组件中再次绑定了一个coo属性 :coo="coo"

继续绑定的属性和 $attrs 中的属性有重复时,继续绑定的属性优先级会更高,所以在test3组件的$attrs里,test2绑定的coo值 会覆盖 test1里绑定的coo值

test3 组件中 props 中接收了一个 boo。所以$attrs传的是剩下的 { "coo": "test2的coo", "doo": "doo" }

test4 组件中 props 中接收了coo。所以$attrs传的是剩下的 { "doo": "doo" }

多级嵌套组件通信 事件传递 描述:

test3组件中 触发执行 log1 方法:

在这里插入图片描述

在这里插入图片描述

test3 组件中再次绑定了一个log1事件 @log1="log1"

继续绑定的事件和 $listeners 中的事件有重复时,不会被覆盖,重复绑定的事件都会被执行,会通过 事件冒泡 执行重复绑定的事件,所以在test4组件中监听触发log1事件,会 先执行 test3组件的log1方法,后执行 test1组件的log1方法

test4组件中 触发执行 log1 方法(事件冒泡执行)

在这里插入图片描述

在这里插入图片描述

provide / inject 依赖注入

这种方式就是 Vue 中的依赖注入,该方法适用隔代组件通信。

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

在层数很深的情况下,可以使用这种方法来进行传值,就不用一层一层的传递了。

provide / inject是Vue提供的两个钩子,和datamethods是同级的。

  • provide钩子用来发送数据或方法
  • inject钩子用来接收数据或方法
// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

注意:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

provideinject 响应式的例子:

provider.vue (父组件)

<template>
  <div>
    <label>父组件输入框:</label>
    <input v-model="level.name" @change="levelChange(level.name)" />
    <!-- 子组件 -->
    <child></child>
  </div>
</template>
<script>
import child from "./components/child.vue";
export default {
  components: {
    child,
  },
  provide() {
    return {
      userLevel: this.level,
    };
  },
  data() {
    return {
      level: { name: "初始化" },
    };
  },
  methods: {
    levelChange(val) {
      this.userLevel = this.level;
      console.log(val); //可以打印出对象属性name值改变了
    },
  },
};
</script>

child.vue (子组件)

<template>
  <div>
    <p>子组件接收数据:{{ userLevel.name }}</p>
    <label>子组件输入框:</label><input type="text" v-model="userLevel.name" />
  </div>
</template>

<script>
export default {
  inject: {
    userLevel: {
      default: () => {},
    },
  },
  data() {
    return {};
  },
};
</script>

在这里插入图片描述

provide和inject实现响应,父组件的数据修改影响了子组件的更新,子组件的数据修改同样影响了父组件的更新。

Vuex 状态管理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

每一个 Vuex 应用的核心就是 store(仓库),store 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

查看全部文章

博文系列目录

  • JavaScript 深入系列
  • JavaScript 专题系列
  • JavaScript 基础系列
  • 网络系列
  • 浏览器系列
  • Webpack 系列
  • Vue 系列
  • 性能优化与网络安全系列
  • HTML 应知应会系列
  • CSS 应知应会系列

交流

各系列文章汇总:https://github.com/yuanyuanbyte/Blog

我是圆圆,一名深耕于前端开发的攻城狮。

weixin

yuanyuanbyte avatar Nov 21 '21 07:11 yuanyuanbyte