vue 组件通信方式

1,938 阅读4分钟

前言

组件通信分为:父子组件通信、隔代组件通信、兄弟组件通信。

props 和 $emit

适用于父子组件,这种方法是 Vue 组件的基础。

<!-- Parent.vue -->
<template>
  <div>
    <child :value="text" @input="handleValue"></child>
    <h1>{{text}}</h1>
  </div>
</template>
<script>
import Child from "./Child.vue";
export default {
  data () {
    return {
      text: "父组件!"
    }
  },
  components :{
    Child
  },
  methods: {
    handleValue (value) {
      this.text = value;
    }
  }
}
</script>

<!-- Child.vue -->
<template>
  <input type="text" @input="handleInput" :value="value">
</template>
<script>
export default {
  props: ['value'],
  methods: {
    handleInput (e) {
      this.$emit('input', e.target.value);
    }
  }
}
</script>

父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
子组件 $emit 触发当前实例上的事件,附加参数都会传给监听器回调。

ref 和 $parent / $children

适用父子组件通信

<!-- Parent.vue -->
<template>
  <div>
    <button @click="handleClick">+3</button>
    <p>{{value}}</p>
    <child ref="child"></child>
  </div>
</template>
<script>
import Child from './Child.vue'
export default {
  data () {
    return {
      value: 0
    }
  },
  components: {
    Child
  },
  methods: {
    handleClick () {
      this.$refs.child.changeNumber (this.value += 3)
    },
    changeValue (value) {
      this.value = +value
    }
  }
}
</script>

通过 $refs 找到子组件,调用子组件中的函数。

<!-- Child.vue -->
<template>
  <input type="text" :value="number" @input="handleInput">
</template>
<script>
export default {
  data () {
    return {
      number: 0
    }
  },
  methods: {
    handleInput (e) {
      let number = e.target.value
      this.$parent.changeValue(number?number:0);
    },
    changeNumber (value) {
      this.number = value
    }
  }
}
</script>

child 组件通过 $parent 找到父组件,调用父组件中的函数。

EventBus

适用于父子、隔代、兄弟组件通信。

全局 EventBus

//  main.js
import Vue from 'vue'
Vue.prototype.$EventBus = new Vue()

在 Vue 原型新增 $EventBus 属性赋值一个新的 Vue 实例,用作事件总线。

<!-- Parent.vue -->
<template>
  <div>
    <child1></child1>
    <child2></child2>
  </div>
</template>
<script>
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue"
export default {
  components: {
    Child1,
    Child2
  }
}
</script>
<!-- Child1.vue -->
<template>
  <input type="text" @input="handleInput" placeholder="请输入内容">
</template>
<script>
export default {
  methods: {
    handleInput (e) {
      this.$EventBus.$emit("handleShow", e.target.value);
    }
  }
}
</script>

Child1 组件中利用 $emit 在事件总线上创建一个 handleShow 事件,并且传入参数 e.target.value。

<!-- Child2.vue -->
<template>
  <p>{{text}}</p>
</template>
<script>
export default {
  data () {
    return {
      text: ""
    }
  },
  mounted () {
    this.$EventBus.$on("handleShow", this.handleContent);
  },
  beforeDestroy() {
    this.$EventBus.$off("handleShow", this.handleContent);
  },
  methods: {
    handleContent (text) {
      this.text = text;
    }
  }
}
</script>

Child2 组件中利用 $on 订阅事件总线的 handleShow 事件,触发相应函数。通过 $off 取消订阅 handleShow 事件。

局部 EventBus

//  eventBus.js
import Vue from 'vue'
export const EventBus = new Vue();

创建一个 eventBus.js 文件,导出一个 vue 实例用作事件总线。

<!-- Child1.vue -->
<script>
import {EventBus} from "../eventBus.js";
export default {
  methods: {
    handleInput (e) {
      EventBus.$emit("handleShow", e.target.value);
    }
  }
}
</script>

在所需的组件中导入事件总线。$emit 创建事件。

<!-- Child2.vue -->
<script>
import {EventBus} from "../eventBus.js";
export default {
  data () {
    return {
      text: ""
    }
  },
  mounted () {
    EventBus.$on("handleShow", this.handleContent);
  },
  beforeDestroy() {
    EventBus.$off("handleShow", this.handleContent);
  },
  methods: {
    handleContent (text) {
      this.text = text;
    }
  }
}
</script>

在所需的组件中导入事件总线。$on 订阅事件。

$attrs 和 $listeners

适用于隔代组件通信。

首先要明白 inheritAttrs 选项的作用:

  • 默认为true,当父组件绑定参数没有被子组件的 props 接受,则会显示在子组件的根元素上。下图是子组件没有通过 props 接收 value2。
  • false,则与上述情况相反,不会显示在根元素上,并且被子组件的 $attrs 属性所保存下来。
  • 注意:这个选项不影响 class 和 style 绑定。Vue官方文档
<!-- Parent.vue -->
<template>
  <div>
    <child1 
      :value1="value1" :value2="value2"
      @fun1="fun1" @fun2="fun2"></child1>
  </div>
</template>
<script>
import Child1 from "./Child1.vue"
export default {
  data () {
    return {
      value1: "父组件值1111",
      value2: "父组件值2222"
    }
  },
  components: {
    Child1
  },
  methods: {
    fun1 (value) { console.log(value); },
    fun2 (value) { console.log(value); }
  }
}
</script>
<!-- Child1.vue -->
<template>
  <div>
    <button @click="fun">点击触发父级事件fun1</button>
    <p>{{value1}}</p>
    <hr>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
  inheritAttrs: false,
  props: ['value1'],
  components: {
    Child2
  },
  methods: {
    fun () { this.$emit('fun1', "子组件参数"); }
  }
}
</script>

v-bind="$attrs",使得 Child1 组件没有接收的数据传递给了 Child2 组件。

v-on="$listeners",使得 Child2 组件也可以通过 $emit 方法来调用父作用域中的v-on 事件监听器(不含 .native 修饰器) 。

<!-- Child2.vue -->
<template>
  <div> 
    <button @click="fun">点击触发隔代组件事件</button>
    <p>{{value2}}</p>
  </div>
</template>
<script>
export default {
  props: ['value2'],
  methods: {
    fun () { this.$emit('fun2', "隔代组件参数"); }
  }
}
</script>

Child2 组件接收 value2 且调用隔代组件的事件。

Vuex

适用于父子、隔代、兄弟组件通信。

准备工作

  • 安装 vuex

npm install vuex --save

  • 在 main.js 文件引入 vuex。

import Vuex from 'vuex'
Vue.use(Vuex)

Vuex 核心

  • state
state: {
    count: 0
}

我的理解就是数据,整个应用中的共享数据。

  • getter
getters: {
    getCount: state => state.count,
    //  例如
    sum: state => state.count + 10
}

虽然可以直接通过 store.state.count 获取属性,但如果在获取数据之前要经过特殊处理,getter 无疑是非常有用的,上述代码 sum 给 count 加 10 再进行返回。

  • mutation
mutation: {
    addCountSync: state => state.count++
}

通过 commit 提交 mutation 修改 state
“更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。”这句话是Vue官网上的原话。看到这句话相信很多人都会去尝试直接修改 state 来测试,然后发现这样也可以对 state 进行修改,但是在 Vue Devtools 调试工具的Vuex状态没有变更,这对于找错误和维护方面带来困难, 并且在开启严格模式下会报错

一条重要的原则就是要记住 mutation 必须是同步函数。 为什么呢

  • action
actions: {
    addCount ({commit}) {
        setTimeout(() => commit('addCountSync') ,2000);
    }
}

只有同步没有异步怎么行呢,毕竟大应用项目数据处理都会有异步操作。actions 可以包含异步操作,通过提交 mutation 来修改状态,而不是直接变更状态。上述代码就是设定定时器 2秒后提交 mutation 操作。
上述代码参数通过解构得到 commit 用于提交 mutation。 通过 dispatch 提交 action 操作。

合并上述代码尝试一个简单示例。

//  main.js
const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    getCount: state => state.count,
    sum: state => state.count + 10
  },
  mutations: {
    addCountSync: state => state.count++
  },
  actions: {
    addCount ({commit}) {
      setTimeout(() => commit('addCountSync') ,2000);
    }
  }
});
new Vue({
    ...
    store
})

创建一个 store,创建 Vue 实例应用中加入这个 store,这样应用下的组件就能获取或修改 storestate

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="handleClickSync">同步改变count</button><br>
    <button @click="handleClick">异步改变count</button>
  </div>
</template>
<script>
export default {
  computed: {
    count () {
      return this.$store.getters.getCount
    }
  },
  methods: {
    handleClickSync () {
      this.$store.commit('addCountSync');
    },
    handleClick () {
      this.$store.dispatch('addCount');
    }
  }
}
</script>
  • module

当应用变得非常复杂时,store 对象就有可能变得相当臃肿。这时把 store 划分模块,化繁为简。

Vuex 我简单记录了一小部分,完整地学习还是要移步去 Vue 官网

provide 和 inject

适用于隔代通信。

在祖先组件中通过 provide 把数据注入。

<!-- Parent.vue -->
<template>
  <div>
    <h1>父级数据:</h1>
    <p>{{person.name}}, {{age}}, {{city}}</p>
    <button @click="handleClick">父级改变数据</button>
    <hr>
    <child></child>
  </div>
</template>
<script>
import Child from "./Inject.vue"
export default {
  name: "aaa",
  components: {
    Child
  },
  data () {
    return {
      person: {
        name: "最初名字"
      },
      age: 10,
      city: "广州"
    }
  },
  provide () {
    return {
      person: this.person,
      age: this.age,
      getCity: () => this.city,
      setCity: (value) => this.city = value
    }
  },
  methods: {
    handleClick () {
      console.log("点击父级修改按钮!")
      this.person.name = "父组件名字"
      this.age = 20
      this.city = "深圳"
    }
  },
  created () {
    console.log("父级数据:",this.person.name, this.age, this.city)
  },
  updated() {
    console.log("更新后的父级数据:",this.person.name, this.age, this.city)
  }
}
</script>

在子孙组件中通过 inject 提取所需要的数据。

<!-- Child.vue -->
<template>
  <div>
    <h1>子组件数据:</h1>
    <p>{{person.name}}, {{age}}, {{city}}</p>
    <button @click="handleClick">子组件修改数据</button>
  </div>
  </template>
<script>
export default {
  name: "bbb",
  inject: ['person', 'age', 'getCity','setCity'],
  computed: {
    city () {
      return this.getCity()
    }
  },
  methods: {
    handleClick () {
      console.log("点击子级修改按钮!")
      this.person.name = "子组件名字"
      this.age = 30
      this.setCity('上海');
    }
  },
  created () {
    console.log("子级数据:",this.person.name, this.age, this.city)
  },
  updated() {
    console.log("更新后的子级数据:", this.person.name, this.age, this.city)
  }
}
</script>

控制台打印结果:

父级数据: 最初名字 10 广州
子级数据: 最初名字 10 广州

//  点击父级修改按钮
点击父级修改按钮! 
更新后的子级数据: 父组件名字 10 深圳
更新后的父级数据: 父组件名字 20 深圳

//  点击子级修改按钮
点击子级修改按钮!
更新后的子级数据: 子组件名字 30 上海 
更新后的父级数据: 子组件名字 20 上海

上述代码结果发现 age 没有响应,只是一开始把父组件 age 赋值给了子组件,而 person 这个对象和通过计算属性的 city 值能响应。

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

vue 2.5.0+ 版本还可以为 inject 设置默认值使其变为可选项。Vue官方文档