Vue之组件通信


组件通信

props/$emit

在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。

父组件向子组件传递数据

代码示例

父组件

在父组件里引入子组件,然后通过子组件标签属性来传递数据

<template>
    <div>
        <h1>Parent</h1>
        <my-child my-message="from Parent Component"></my-child>
    </div>
</template>

<script>
import MyChild from './Child'
export default {
    components: {
        MyChild,
    }
}
</script>

<style>
</style>

子组件

在子组件显式地用 props 选项声明它预期的数据并使用

<template>
    <div>
    <h1>Child</h1>
    <h3>{{ myMessage }}</h3>
  </div>
</template>

<script>
export default {
    props: {
       myMessage: {
        type: String,
        default: ''
      }
    }
}
</script>

<style>
</style>

注意事项

camelCase vs. kebab-case

HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名)。

动态props

与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

<my-child :my-message="'from Parent Component'"></my-child>

以上是使用v-bind的简写方式。

值得注意的是,使用动态绑定的数据时,第一层引号内的数据是一个表达式:

<my-child my-message="1"></my-child>
<my-child :my-message="1"></my-child>

上面两个传递的数据是不同的,第一个传递的是字符串1,后面的才是传递数字1。

单向数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

在两种情况下,我们很容易忍不住想去修改 prop 中数据:

  1. Prop 作为初始值传入后,子组件想把它当作局部数据来用
  2. Prop 作为原始数据传入,由子组件处理成其它数据输出

对这两种情况,正确的应对方式是:

  1. 定义一个局部变量,并用 prop 的值初始化它:

    export default {
       data() {
           return {
               message: this.myMessage
           }
       },
       props: {
          myMessage: {
           type: String,
           default: ''
         }
       }
    }
  1. 定义一个计算属性,处理 prop 的值并返回:

    export default {
        computed: {
            message() {
                return 'I am' + this.myMessage
            }
        }
        props: {
           myMessage: {
            type: String,
            default: ''
          }
        }
    }

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。即便引用类型可以,也不要利用这个特性,记住一个原则:组件的数据状态在组件内部管理维护,不要在其他位置去修改它

子组件向父组件传递数据

代码示例

父组件

  1. 在父组件中提供一个子组件内部发布的事件处理函数

  2. 在使用子组件的模板的标签上订阅子组件内部发布的事件

<template>
    <div>
        <h1>Parent</h1>
        <my-child @transMessage="showMessage"></my-child>
        <p>{{ message }}</p>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    data() {
        return {
            message: ''
        }
    },
    methods: {
        showMessage(val) {
            this.message = val
        },
    },
    components: {
        myChild
    }
}
</script>

<style>
</style>

子组件

  1. 在子组件中调用 $emit() 方法发布一个事件
<template>
    <div>
    <h2>Child</h2>
  </div>
</template>

<script>
export default {
    mounted() {
    this.$emit('transMessage', 'I am from Child component')
  }
}
</script>

<style>
</style>

$parent/$children

代码示例

子组件

<template>
    <div>
    <h2>Child</h2>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  },
    mounted() {
    // 通过 $parent 获取当前实例的父实例
    this.message = this.$parent.parentMessage
  }
}
</script>

<style>
</style>

父组件

<template>
    <div>
        <h1>Parent</h1>
        <my-child></my-child>
        <p>{{ message }}</p>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    data() {
        return {
            message: '',
            parentMessage: 'parent message'
        }
    },
    mounted() {
        // 通过 $children 获取当前实例的直接子组件
        // 获取的数据是个数组,每个子组件是该数组的一个元素
        this.message = this.$children[0].childMessage
    },
    components: {
        myChild
    }
}
</script>

<style>
</style>

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

provide/inject(祖先组件向后代组件传递)

2.2.0 新增

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

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

父组件

<template>
    <div>
        <h1>Parent</h1>
        <my-child></my-child>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    // 父组件提供一个 foo 对象 
    provide: {
        foo: {
            name: 'Jack',
            age: 18
        }
    },
    components: {
        myChild
    }
}
</script>

<style>
</style>

子组件

<template>
    <div>
    <h2>Child</h2>
    <ul v-for="item in foo">
      <li>{{ item }}</li>
    </ul>
    <hr>
  </div>
</template>

<script>
export default {
  // 子组件注入 foo
  inject: ['foo'],
  data() {
    return {
      message: null,
    }
  },
    mounted() {
    this.message = this.foo
  }
}
</script>

<style>
</style>

注意: 这里不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据,而不局限于只能从当前父组件的props属性中回去数据

ref/$ref(子组件向父组件传递)

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

父组件

  1. 在使用子组件的模板的标签上通过ref为子组件赋予一个 ID 引用
  2. 通过 $ref 获取子组件实例
<template>
    <div>
        <h1>Parent</h1>
        <my-child ref="child"></my-child>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    components: {
        myChild
    },
    mounted() {
        console.log(this.$refs.child)
    }
}
</script>

<style>
</style>

子组件

<template>
    <div>
    <h2>Child</h2>
    <hr>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'child message',
    }
  },
}
</script>

<style>
</style>

$attrs/$listeners

在非直接父子组件之间的通信,如A组件下面有B组件,B组件下面有C组件,我们想将A组件的数据传递给C组件或者将C组件的数据传递给A组件,可以有以下几种方法:

  • props:使用props绑定来进行一级一级的信息传递,如果D组件中状态改变需要传递数据给A,使用事件系统一级级往上传递(繁琐
  • $parent/$children:多个$parent或者$children一层一层向上或向下传递(节制地使用这两个实例属性
  • eventBus:使用eventBus,这种情况下还是比较适合使用,但是碰到多人合作开发时,代码维护性较低, 可读性也低
  • Vuex:使用Vuex来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用Vuex处理感觉有点大材小用

vue2.4中,为了解决该需求,引入了$attrs$listeners , 新增了inheritAttrs 选项。

祖先组件向后代组件传递

vue官方inheritAttrs 地说明:

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrsfalse,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上。

注意:这个选项不影响 classstyle 绑定。

通俗的讲就是:inheritAttrs 属性默认为true,这时父作用域上的非props属性回退为普通的HTML attribute,如果设置为false,则不会回退为普通的HTML attribute,这些attribute会被子组件的实例属性 $attrs 收集起来。这些属性不包括classstyle

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

A组件

<template>
    <div>
        <h1>A</h1>
        <com-b :a="1" :b="2" :c="3"></com-b>
    </div>
</template>

<script>
import comB from './ComB'
export default {
    data() {
        return {
          name: 'A',
        }
    },
    components: {
        comB
    },
}
</script>

<style>
</style>

B组件

<template>
    <div>
        <h1>B</h1>
        <com-c v-bind="$attrs"></com-c>
    </div>
</template>

<script>
import comC from './ComC'
export default {
    data() {
        return {
            name: 'B',
        }
    },
    components: {
        comC
    },
}
</script>

<style>
</style>

C组件

<template>
    <div>
        <h1>C</h1>
    </div>
</template>

<script>
export default {
    inheritAttrs: false,
    data() {
        return {
            name: 'C',
        }
    },
    props: {
        a: {
            type: Number
        }
    }
    mounted() {
        console.log(this.$attrs) // {b: 2, c: 3}
    }
}
</script>

<style>
</style>

后代组件向祖先组件传递

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

A组件

<template>
  <div>
    <my-parent @test1="showTest1" @test2="showTest2"></my-parent>
  </div>
</template>

<script>
import MyParent from './views/Parent'
export default {
    data() {
        return {
            name: 'app',
        }
    },
    components: {
        MyParent,
    },
    methods: {
        showTest1() {
            console.log('test1...')
        },
        showTest2(val) {
            console.log('test2 ' + val)
        }
    }
}
</script>

<style>
</style>

B组件

<template>
    <div>
        <h1>Parent</h1>
        <my-child v-on="$listeners"></my-child>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    data() {
        return {
            name: 'parent',
        }
    },
    components: {
        myChild
    }
}
</script>

<style>
</style>

C组件

<template>
  <div>
    <h2>Child</h2>
  </div>
</template>

<script>
export default {
  created() {
    console.log(this.$listeners)
    this.$emit("test1")
    this.$emit("test2", 'from child component')
  }
}
</script>

<style>
</style>

eventBus

eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。

$emit$on的事件必须使用一个空的 Vue 实例作为中央事件总线的实例上,才能够触发,否则会出现子组件$emit后父组件没有监听到函数的变化

初始化

首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它

// bus.js
import Vue from 'vue'
export default new Vue()

发送事件

假设我们有两个兄弟组件:Elder-brotherYounger-brother,我们需要在这两个兄弟组件之间传递数据

<template>
    <div>
        <elder-brother></elder-brother>
        <younger-brother></younger-brother>
  </div>
</template>

<script>
import ElderBrother from './views/Elder-brother'
import YoungerBrother from './views/Younger-brother'
export default {
    components: {
        ElderBrother,
        YoungerBrother
    }
}
</script>

<style>
</style>

Elder-brother组件:

<template>
    <div>
        <h1>elder brother</h1>
        <button @click="sendMessage">send</button>
    </div>
</template>

<script>
import EventBus from '../util/bus'
export default {
    data() {
        return {
            message: 'from Elder-brother component',
        }
    },
    methods: {
        sendMessage() {
            // 利用 $emit 发布事件传递数据
              EventBus.$emit('transMessage', this.message)
        },
    },
}
</script>

<style>
</style>

接收事件

Younger-brother组件:

<template>
  <h1>younger brother</h1>
</template>

<script>
import EventBus from '../util/bus'
export default {
  mounted() {
    // 利用 $on 接收事件,并执行回调函数
    EventBus.$on('transMessage', (param) => {
      console.log(param)
    })
  }
}
</script>

<style>

</style>

这样就完成了兄弟组件之间的数据传递。

移除事件监听者

import { eventBus } from 'bus.js'
EventBus.$off('transMessage')

如果想精确移除某个事件的监听器,还需要传递一个回调函数:

<template>
  <div>
    <h1>younger brother</h1>
  <button @click="destroy">destroy</button>
  </div>
</template>

<script>
import EventBus from '../util/bus'
export default {
  mounted() {
    EventBus.$on('transMessage', this.fn1)
    EventBus.$on('transMessage', this.fn2)
  },
  methods: {
    fn1(param) {
      console.log(param + 'fn1')
    },
    fn2(param) {
      console.log(param + 'fn2')
    },
    destroy() {
      EventBus.$off('transMessage', this.fn1)
    }
  }
}
</script>

<style>

</style>

localStorage / sessionStorage

这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。 通过window.localStorage.getItem(key)获取数据 通过window.localStorage.setItem(key,value)存储数据

注意用JSON.parse() / JSON.stringify() 做数据格式转换 localStorage / sessionStorage可以结合vuex,实现数据的持久保存,同时使用vuex解决数据和状态混乱问题。

v-model

v-model只能实现父子组件之间的数据传递

父组件

<template>
    <div>
        <h1>Parent</h1>
        <p>{{ message }}</p>
        <my-child v-model="message"></my-child>
    </div>
</template>

<script>
import myChild from './Child'
export default {
    data() {
        return {
            message: 'parent',
        }
    },
    components: {
        myChild
    }
}
</script>

<style>
</style>

子组件

<template>
    <div>
    <h2>Child</h2>
    <input type="text" v-model="myMessage" @input="changeValue">
  </div>
</template>

<script>
export default {
  props: {
    value: String
  },
  data() {
    return {
      myMessage: this.value,
    }
  },
  methods: {
    changeValue() {
      this.$emit('input', this.myMessage)
    }
  }
}
</script>

<style>
</style>
  1. 在 Parent 组件中,我们给自定义的 Child 组件实现了 v-model 绑定了 message 属性。此时相当于给 Child 组件传递了 value 属性和绑定了 input 事件
  2. 顺理成章,在定义的 child 组件中,可以通过 props 获取 value 属性,根据 props 单向数据流的原则,又将 value 缓存在了 data 里面的 myMessage 上,再在 input 上通过 v-model 绑定了 myMessage 属性和一个 change 事件。当 input 值变化时,就会触发 change 事件,处理 parent 组件通过 v-model 给 child 组件绑定的 input 事件,触发 parent 组件中 message 属性值的变化,完成 child 子组件改变 parent 组件的属性值。

Vuex

还没了解Vuex,内容应该挺多的,等熟悉了再仔细总结一下。

参考

Vue 组件通信方式全面详解

vue中8种组件通信方式, 值得收藏!


评论
 上一篇
JavaScript事件循环机制 JavaScript事件循环机制
事件循环(Event Loop) 事件循环是JavaScript的执行机制 。 前置知识js是单线程语言 什么是单线程? 主程序只有一个线程,即同一时间片段内其只能执行单个任务。 单线程意味着什么? 单线程就意味着,所有任务都需要排队,
2020-10-03
下一篇 
CSS基础之flex/grid CSS基础之flex/grid
这篇文章内容来自阮一峰的Flex 布局教程:语法篇和CSS Grid 网格布局教程,在一些微小的地方添加了一些注意事项。写这篇的目的是为了方便自己回顾,所以将图片去掉了,如果想更方便的学习推荐去阮老师的博客。 flex基本概念采用 Fl
2020-08-23
  目录