组件通信

组件通信一般分为以下几种情况:

  • 父子组件通信
  • 兄弟组件通信
  • 跨多层级组件通信
  • 任意组件

对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。

父子通信

父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。

当然我们还可以通过访问 $parent$children$refs 对象来访问组件实例中的方法和数据。

兄弟组件通信

对于这种情况可以通过查找父组件中的子组件实现,也就是 this.$parent.$children,在 $children中可以通过组件 name 查询到需要的组件实例,然后进行通信。

跨多层级组件通信

对于这种情况可以使用 Vue 2.2 新增的 API provide / inject,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。

假设有父组件 A,然后有一个跨多层级的子组件 B

  1. // 父组件 A
  2. export default {
  3. provide: {
  4. data: 1
  5. }
  6. }
  7. // 子组件 B
  8. export default {
  9. inject: ['data'],
  10. mounted() {
  11. // 无论跨几层都能获得父组件的 data 属性
  12. console.log(this.data) // => 1
  13. }
  14. }

另外也可以自定义dispatch、boardcast方法实现

  1. // dispatch 向上传递
  2. Vue.prototype.$dispatch = function (eventName, data) {
  3. let parent = this.$parent
  4. // 查找父元素
  5. while (parent) {
  6. if (parent) {
  7. // 父元素用$emit触发
  8. parent.$emit(eventName, data)
  9. // 递归查找父元素
  10. parent = parent.$parent
  11. } else {
  12. break
  13. }
  14. }
  15. };
  16. // boardcast 向下传递
  17. Vue.prototype.$boardcast = function (eventName, data) {
  18. boardcast.call(this, eventName, data)
  19. };
  20. function boardcast(eventName, data) {
  21. this.$children.forEach(child => {
  22. // 子元素触发$emit
  23. child.$emit(eventName, data)
  24. if (child.$children.length) {
  25. // 递归调用,通过call修改this指向 child
  26. boardcast.call(child, eventName, data)
  27. }
  28. })
  29. }

任意组件通信

这种方式可以通过 Vuex 或者 Event Bus(事件总线) 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况

  1. // Event Bus
  2. class Bus {
  3. constructor() {
  4. // {
  5. // eventName1:[fn1,fn2],
  6. // eventName2:[fn3,fn4],
  7. // }
  8. this.callbacks = {}
  9. }
  10. $on(name, fn) {
  11. this.callbacks[name] = this.callbacks[name] || []
  12. this.callbacks[name].push(fn)
  13. }
  14. $emit(name, args) {
  15. if (this.callbacks[name]) {
  16. // 存在 遍历所有callback
  17. this.callbacks[name].forEach(cb => cb(args))
  18. }
  19. }
  20. }
  21. // 可以使用 Vue 替代 Bus 类,因为它已经实现了相应的功能
  22. Vue.prototype.$bus = new Bus()

插槽

匿名插槽

  1. // comp1
  2. <div>
  3. <slot></slot>
  4. </div
  5. // parent
  6. <Comp2>
  7. <div>默认插槽</div>
  8. </Comp2>

具名插槽

  1. // comp2
  2. <div>
  3. <h3>
  4. <slot></slot>
  5. </h3>
  6. <slot name="content"></slot>
  7. </div>
  8. // parent
  9. <Comp2>
  10. <div>默认插槽</div>
  11. <!-- slot="插槽名" -->
  12. <template slot="content">content 插槽</template>
  13. </Comp2>

作用域插槽

  1. // comp3
  2. <div>
  3. <h3>作用域插槽</h3>
  4. <!-- 通过绑定指定作用域 -->
  5. <slot :foo="foo"></slot>
  6. </div>
  7. // parent
  8. <Comp3>
  9. <!-- slot-scope="作用域上下文" -->
  10. <template slot-scope="ctx">来自子组件数据:{{ctx.foo}}</template>
  11. </Comp3>

Vue2.6.0新语法

Vue2.6.0 之后采用全新v-slot语法取代之前的slot、slot-scope

  1. <!-- 2.6.0 新语法 -->
  2. <!--
  3. 在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。
  4. 它取代了 slot 和 slot-scope属性。
  5. 在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并
  6. 以 v-slot:name 的参数的形式提供其名称。
  7. 一个不带 name 的 <slot> 插槽会带有默认的名字“default”。
  8. 任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
  9. 推荐为所有的插槽使用完整的基于 <template v-slot:default> 的语法。
  10. -->
  11. <Comp1>默认插槽</Comp1>
  12. <Comp2>
  13. <!-- 默认插槽用default -->
  14. <template v-slot:default>默认插槽</template>
  15. <!-- v-slot:插槽名 -->
  16. <template v-slot:content>content 插槽</template>
  17. </Comp2>
  18. <Comp3>
  19. <!-- v-slot:插槽名="作用域上下文" -->
  20. <template v-slot:default="ctx">来自子组件数据:{{ctx.foo}}</template>
  21. </Comp3>

递归组件

  • 递归组件必须要有结束条件
  • name对递归组件时必要的(递归组件调用自己不需要注册,需要提供name)
  1. <template>
  2. <div>
  3. <Node :data="data"></Node>
  4. </div>
  5. </template>
  6. <script>
  7. import Node from "./Node.vue";
  8. export default {
  9. data() {
  10. return {
  11. data: {
  12. id: "1",
  13. title: "递归组件",
  14. children: [
  15. { id: "1-1", title: "使用方法" },
  16. { id: "1-2", title: "注意事项" }
  17. ]
  18. }
  19. };
  20. },
  21. components: {
  22. Node
  23. }
  24. };
  25. </script>
  26. // Node.vue
  27. <template>
  28. <div>
  29. <h3>{{data.title}}</h3>
  30. <!-- 必须有结束条件,这里的 v-for 可以作为结束条件 -->
  31. <Node v-for="d in data.children" :key="d.id" :data="d"></Node>
  32. </div>
  33. </template>
  34. <script>
  35. export default {
  36. name: "Node", // name对递归组件是必要的
  37. props: {
  38. data: {
  39. type: Object,
  40. require: true
  41. }
  42. }
  43. };
  44. </script>

v-model 和 .sync

v-model

  1. <!-- v-model是语法糖 -->
  2. <input v-model="username">
  3. <!-- 默认等效于下面这行 -->
  4. <input :value="username" @input="username = $event.target.value">
  5. <!-- v-model是语法糖 -->
  6. <input type="checkbox" v-model="check">
  7. <!-- 默认等效于下面这行-->
  8. <input type="checkbox" :checked="check" @change="check = $event.target.checked">

自定义 input 组件

  1. <template>
  2. <input type="text" :value="value" @input="$emit('input', $event.target.value)">
  3. </template>
  4. <script>
  5. export default {
  6. name: 'c-input',
  7. // 默认,不需要写
  8. // model: {
  9. // prop: 'value',
  10. // event: 'input'
  11. // }
  12. props: {
  13. value: String
  14. }
  15. }
  16. </script>
  17. <style>
  18. </style>

自定义 checkbox 组件

  1. <template>
  2. <div>
  3. <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)">
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: 'c-checkbox',
  9. model: {
  10. prop: 'checked',
  11. event: 'change'
  12. },
  13. props: {
  14. checked: Boolean
  15. }
  16. }
  17. </script>
  18. <style>
  19. </style>

.sync

  1. <c-input-sync :value.sync="username"></c-input-sync>
  2. <c-input-sync :value="username" @update:value="username = $event"></c-input-sync>
  3. <!-- 这里绑定属性名称可以随意更改,update 相应的属性名也会变化 -->
  4. <c-input-sync :foo="username" @update:foo="username = $event"></c-input-sync>
  1. <template>
  2. <input type="text" :value="value" @input="$emit('update:value', $event.target.value)" />
  3. </template>
  4. <script>
  5. export default {
  6. name: 'c-input-sync',
  7. props: {
  8. value: String
  9. }
  10. }
  11. </script>
  12. <style>
  13. </style>

自定义表单组件

Input

  • 双向绑定:@input、:valu
  • 派发校验事件

inheritAttrs:默认情况下父组件中不被认作 props 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上,通过设置 inheritAttrs: false,就不会绑定到根元素上。

attrs:这些属性会可以直接绑定到非根元素上。

  1. <c-input v-model="model.username" autocomplete="off" placeholder="输入用户名"></c-input>
  1. <template>
  2. <div>
  3. <input :value="value" @input="onInput" v-bind="$attrs">
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. inheritAttrs: false,
  9. props: {
  10. value: {
  11. type: String,
  12. default: ""
  13. }
  14. },
  15. methods: {
  16. onInput(e) {
  17. this.$emit("input", e.target.value);
  18. this.$parent.$emit("validate");
  19. }
  20. }
  21. };
  22. </script>
  23. <style lang="scss" scoped>
  24. </style>

FormItem

  • 给Input预留插槽 - slot
  • 能够展示label和校验信息
  • 能够进行校验
  1. <template>
  2. <div>
  3. <label v-if="label">{{label}}</label>
  4. <slot></slot>
  5. <p v-if="errorMessage">{{errorMessage}}</p>
  6. </div>
  7. </template>
  8. <script>
  9. import Schema from "async-validator";
  10. export default {
  11. // 接收数据
  12. inject: ["form"],
  13. props: {
  14. label: {
  15. type: String,
  16. default: ""
  17. },
  18. prop: {
  19. type: String
  20. }
  21. },
  22. data() {
  23. return {
  24. errorMessage: ""
  25. };
  26. },
  27. mounted() {
  28. // 勘误
  29. // this.$on("validate", this.validate);
  30. // 上面的代码 返回的 rejected 状态的 promise 没有被catch捕获,控制台会报错
  31. this.$on('validate', () => {
  32. this.validate()
  33. })
  34. },
  35. methods: {
  36. validate() {
  37. // 做校验
  38. const value = this.form.model[this.prop];
  39. const rules = this.form.rules[this.prop];
  40. // npm i async-validator -S
  41. const desc = { [this.prop]: rules };
  42. const schema = new Schema(desc);
  43. // return的是校验结果的Promise
  44. return schema.validate({ [this.prop]: value }, errors => {
  45. if (errors) {
  46. this.errorMessage = errors[0].message;
  47. } else {
  48. this.errorMessage = "";
  49. }
  50. });
  51. }
  52. }
  53. };
  54. </script>
  55. <style lang="scss" scoped>
  56. </style>

Form

  • 给FormItem留插槽
  • 设置数据和校验规则
  • 全局校验
  1. <template>
  2. <div>
  3. <slot></slot>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. // 跨层级传递数据
  9. provide() {
  10. return {
  11. form: this
  12. };
  13. },
  14. props: {
  15. model: {
  16. type: Object,
  17. required: true
  18. },
  19. rules: {
  20. type: Object
  21. }
  22. },
  23. methods: {
  24. validate(cb) {
  25. const tasks = this.$children
  26. .filter(item => item.prop)
  27. .map(item => item.validate());
  28. // 所有任务都通过才算校验通过
  29. Promise.all(tasks)
  30. .then(() => cb(true))
  31. .catch(() => cb(false));
  32. }
  33. }
  34. };
  35. </script>
  36. <style lang="scss" scoped>
  37. </style>

自定义弹框组件

弹框组件通常在当前 vue 实例之外独立存在,通常挂载于 body 上,通过 js 动态创建,不需要再任何组件中声明,可以使用单例模式,避免重复创建。

  • 组件实例创建函数:create函数
  1. import Vue from 'vue';
  2. // create.js 作用是把传递的组件配置(选项对象)转换为组件实例返回
  3. export default function create(Component, props) {
  4. // 先创建Vue实例
  5. const vm = new Vue({
  6. // render 方法会调用 h 方法创建虚拟DOM
  7. render(h) {
  8. // h就是createElement,它返回 VNode
  9. return h(Component, {
  10. props
  11. })
  12. }
  13. }).$mount();
  14. // $mount里面会调render生成VNode,生成的VNode会执行update函数生成DOM
  15. // $mount('body') 直接把 body 作为挂载元素会抛错
  16. // 手动挂载: vm.$el是真实DOM
  17. document.body.appendChild(vm.$el);
  18. // 销毁方法 vm.$children vue实例下的 $children[0] | vm.$root 就是我们创建的组件实例
  19. const comp = vm.$children[0];
  20. comp.remove = function () {
  21. document.body.removeChild(vm.$el);
  22. vm.$destroy();
  23. }
  24. return comp;
  25. }

Notice 组件

  • 插槽预留
  • 标题、内容等属性
  • 自动关闭
  1. <template>
  2. <div class="box" v-if="isShow">
  3. <h3>{{title}}</h3>
  4. <p class="box-content">{{message}}</p>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. props: {
  10. title: {
  11. type: String,
  12. default: ""
  13. },
  14. message: {
  15. type: String,
  16. default: ""
  17. },
  18. duration: {
  19. type: Number,
  20. default: 1000
  21. }
  22. },
  23. data() {
  24. return {
  25. isShow: false
  26. };
  27. },
  28. methods: {
  29. show() {
  30. this.isShow = true;
  31. setTimeout(this.hide, this.duration);
  32. },
  33. hide() {
  34. this.isShow = false;
  35. this.remove();
  36. }
  37. }
  38. };
  39. </script>
  40. <style scoped>
  41. .box {
  42. position: fixed;
  43. width: 100%;
  44. top: 16px;
  45. left: 0;
  46. text-align: center;
  47. pointer-events: none;
  48. }
  49. .box-content {
  50. width: 200px;
  51. margin: 10px auto;
  52. font-size: 14px;
  53. border: blue 3px solid;
  54. padding: 8px 16px;
  55. background: #fff;
  56. border-radius: 3px;
  57. margin-bottom: 8px;
  58. }
  59. </style>
  • 使用
  1. <script>
  2. import Notice from "@/components/notice/KNotice";
  3. export default {
  4. methods: {
  5. submitForm(form) {
  6. this.$refs[form].validate(valid => {
  7. const notice = this.$create(Notice, {
  8. title: "社会你杨哥喊你来搬砖",
  9. message: valid ? "请求登录!" : "校验失败!",
  10. duration: 1000
  11. });
  12. notice.show();
  13. });
  14. }
  15. }
  16. };
  17. </script>

思考:使用单例模式处理弹框组件

  1. // 单例模式处理
  2. const create = (function(){
  3. let comp;
  4. return function(Component, props){
  5. if(!comp) {
  6. const vm = new Vue({
  7. render(h) {
  8. return h(Component, {
  9. props
  10. })
  11. }
  12. }).$mount();
  13. document.body.appendChild(vm.$el);
  14. comp = vm.$children[0];
  15. comp.remove = function () {
  16. if (comp) {
  17. document.body.removeChild(vm.$el);
  18. vm.$destroy();
  19. }
  20. comp = null
  21. }
  22. }
  23. return comp
  24. }
  25. })()
  26. export default create

自定义Tree组件

  1. <template>
  2. <li>
  3. <div @click="toggle">
  4. {{model.title}}
  5. <span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
  6. </div>
  7. <ul v-show="open" v-if="isFolder">
  8. <item
  9. class="item"
  10. v-for="model in model.children"
  11. :model="model" :key="model.title">
  12. </item>
  13. </ul>
  14. </li>
  15. </template>
  16. <script>
  17. export default {
  18. name: "item", // name对递归组件是必要的
  19. props: {
  20. model: {
  21. type: Object,
  22. required: true
  23. }
  24. },
  25. data() {
  26. return {
  27. open: false
  28. };
  29. },
  30. computed: {
  31. // 递归组件必须要有结束条件
  32. isFolder() {
  33. return this.model.children && this.model.children.length;
  34. }
  35. },
  36. methods: {
  37. toggle() {
  38. if (this.isFolder) {
  39. this.open = !this.open;
  40. }
  41. }
  42. }
  43. };
  44. </script>