jannahuang / blog

MIT License
0 stars 0 forks source link

Vue 2 组件间通信方式有哪些? #26

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

props 和 $emit

Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。 Prop 的类型可以是这些原生构造函数:String,Number,Boolean,Array,Object,Date,Function,Symbol。 为了给博文组件传递一个标题,我们可以用一个 props 选项将其包含在该组件可接受的 prop 列表中:

Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件,并使用第二个参数来传值,父级组件就会接收该事件并更新值。 举例:

// 父->子传值 用 props
// parent
<child :message = '父组件的消息'></child>
// child
props:{
  message: {
    type: String,
    default:""
  }
}   
// 子->父  用自定义事件 
// child
this.$emit('reply', '子组件的消息')
// parent
<child @reply='getReply($event)'></child>

$attrs 和 $listeners(2.4.0 新增)

$attrs 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。

$attrs 是一个子组件自带的对象,这个容器对象会存放父组件传过来的且子组件未使用 props 声明接收的数据。

$listeners 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。

$listeners 是一个子组件自带的对象,这个容器对象会存放父组件传过来的非原生事件。

$attrs 与 $listeners 是两个「对象」,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners 里存放的是父组件中绑定的非原生事件。

举例:

//componentA
<template>
  <div class="component-a">
    <component-b :name="name" :tag="tag" :age="age" @click.native="say" @mouseover="sing"></component-b>
  </div>
</template>
<script>
import componentB from "./ComponentB";
export default {
  name: "componentA",
  components: { componentB },
  data() {
    return {
      name: "Foo",
      tag: "Bar",
      age: 18
    };
  },
  methods: {
    say() {},
    sing() {}
  }
};
</script>
//componentB
<template>
  <div class="component-b">
    <component-c v-bind="$attrs" v-on="$listeners"></component-c>
  </div>
</template>
<script>
import ComponentC from "./ComponentC";
export default {
  name: "ComponentB",
  components: { ComponentC },
  props: {
    age: Number
  },
  mounted() {
    console.log(this.$attrs, this.$listeners);
    //{name: "Foo", tag: "Bar"}, {mouseover: ƒ}
  }
};
</script>
//componentC
<template>
  <div class="component-c"></div>
</template>
<script>
export default {
  name: "ComponentC",
  mounted() {
    console.log(this.$attrs, this.$listeners);
    //{name: "Foo", tag: "Bar"} {mouseover: ƒ}
  }
};
</script>

需要注意 Vue 中组件在接受非 Props 属性时,会把属性渲染到 HTML 的原生标签上。在组件的选项中设置 inheritAttrs: false 即可避免这个问题。

<component-b :name="name" :tag="tag"></component-b>
<!-- 上述 Vue 组件的 HTML 渲染结果如下 -->
<div class="component-b" name="Foo" tag="Bar"></div>

EventBus 事件总线

在 Vue 中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。

初始化

可以通过两种方式创建事件总线:

  1. 新创建一个 .js 文件,注册并导出事件总线。
    // event-bus.js
    import Vue from 'vue'
    export const EventBus = new Vue()
  2. 直接在项目中的 main.js 初始化 EventBus,即注册全局的 EventBus。
    // main.js
    Vue.prototype.$EventBus = new Vue()

    实质上 EventBus 是一个不具备 DOM 的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。

发送事件 $emit

EventBus.$emit( eventName, [ …args] ) $emit 可以触发事件。假设有两个 Vue 页面 A 和 B 需要通信,A 页面在按钮上面绑定了点击事件,发送一则消息,想通知 B 页面。

<!-- A.vue -->
<template>
  <button @click="sendMsg()">-</button>
</template>
<script> 
import { EventBus } from "../event-bus.js";
export default {
  methods: {
    sendMsg() {
      EventBus.$emit("aMsg", '来自A页面的消息');
    }
  }
}; 
</script>

接收事件 $on

EventBus.$on( event, callback ) $on 监听事件是否被触发,触发时则执行 callback 事件。

<!-- B.vue -->
<template>
  <p>{{msg}}</p>
</template>

<script> 
import { EventBus } from "../event-bus.js";
export default {
  data(){
    return {
      msg: ''
    }
  },
  mounted() {
    EventBus.$on("aMsg", (msg) => {
      // A发送来的消息
      this.msg = msg;
    });
  }
};
</script>

上述提到如果 EventBus 使用不当,可能会引起“灾难”。如果反复操作页面,EventBus 在监听的时候可能多次触发事件。 通常在使用时,会在使用 $on 前先调用 $off;并且会在 Vue 页面销毁时,同时移除 EventBus 事件监听。

移除事件 $off

EventBus.$off( event, callback )

import {  eventBus } from './event-bus.js'
EventBus.$off('aMsg', {})

全局 EventBus

每次使用 EventBus 时引入 js 文件比较麻烦,可以注册全局的 EventBus。其工作原理就是发布/订阅模式。

var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
  $EventBus: {
    get: function () {
      return EventBus
    }
  }
})
// 或者
Vue.prototype.$EventBus = new Vue()

在这个特定的总线中,$emit 用于触发事件,$on 用于订阅,$off 用于移除事件监听。

vue-component-contact 以上笔记参考 Vue事件总线(EventBus)使用详细介绍

provide 和 inject(2.2.0 新增)

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

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

v-model

父组件通过 v-model 传递值给子组件时,会自动传递一个 value 的 prop 属性,在子组件中通过 this.$emit("input",val) 自动修改 v-model 绑定的值。原理就是利用 props 和 $emit 进行父子组件通信。

在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。

<input v-model="searchText">
<!-- 等价于 -->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

用于组件时,则是要保证这个组件内的 < input> 必须:

自定义组件的 v-model(2.2.0+ 新增)

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
// 正确注册组件之后,即可在单选框、复选框等类型的组件上使用 v-model
<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 < base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。

$parent 和 $children

$parent 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。 $children 是当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。

节制地使用 $parent 和 $children - 它们的主要目的是作为访问组件的应急方法。更推荐用 props 和 events 实现父子组件通信。

Vuex

Vuex 是专门为 Vue.js 设计的状态管理库,可以帮助我们管理共享状态。 vuex 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

核心概念

store 模式

如果有一处需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享。采用一个简单的 store 模式:

var store = {
  debug: true,
  state: {
    message: 'Hello!'
  },
  setMessageAction (newValue) {
    if (this.debug) console.log('setMessageAction triggered with', newValue)
    this.state.message = newValue
  },
  clearMessageAction () {
    if (this.debug) console.log('clearMessageAction triggered')
    this.state.message = ''
  }
}

vue-store 需要注意,所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,也会有一个 log 记录 bug 之前发生了什么。 此外,每个实例/组件仍然可以拥有和管理自己的私有状态:

var vmA = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})
var vmB = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

但是组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变。