父传子

props

父组件
image.png
子组件
image.png

  1. props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
  2. props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。

    provide / inject

  3. provide / inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。● provide 钩子用来发送数据或方法● inject钩子用来接收数据或方法(接收传递的数据,类似peops)

  4. 就是 provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。

例子:

  1. // 父级组件提供 'foo'
  2. <template>
  3. <div>
  4. <div>{{foo}}</div>
  5. <son></son>
  6. </div>
  7. </template>
  8. <script>
  9. import Son from "./Son";
  10. export default {
  11. name: "parent",
  12. components: { Son },
  13. provide() {
  14. return {
  15. foo: this.foo
  16. };
  17. },
  18. data() {
  19. return {
  20. foo: "测试",
  21. };
  22. },
  23. mounted() {
  24. console.log(this.foo)
  25. },
  26. };
  27. </script>
  28. //子级组件,不接收
  29. <template>
  30. <grandSon></grandSon>
  31. </template>
  32. <script>
  33. import grandSon from "./grandSon";
  34. export default {
  35. name: "son",
  36. components: { grandSon },
  37. };
  38. </script>
  39. //孙级组件,接收foo
  40. <template>
  41. <div>{{foo}}</div>
  42. </template>
  43. <script>
  44. export default {
  45. name: "grandSon",
  46. inject: ["foo"],
  47. mounted() {
  48. console.log(this.foo)
  49. },
  50. };
  51. </script>

在这里我们可以发现孙组件越过子组件接收了父组件注入的数据,我们可以理解为爷爷越过爸爸偷偷给孙子买了辣条,这是一组最简单的用法,当层级继续增加时,仍可通过这种方式由父组件直接跨域多个层级向后代组件注入数据。
有一点需要特别注意的是,实际上我们可以将当前组件inject获取的数据直接赋值给它本身的data或props,不过官网提示我们,这是在Vue2.2.1版本才实现的功能,在这之前,必须先进行props和data数据的初始化。

数据的响应更新

在尝试中我们发现,由于Vue的单向流关系,实际上如果在parent中改变了初始传入的foo的值以后,grandSon并不会得到改变后的值,也就是在这个时候,父孙组件的数据出现了不一致的情况,我们肯定是希望拿到的数据是一致的,怎么来解决这个问题呢,官网还有一句提示为我们提供了解释。

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。 也就是指,我们需要人为的将这组数据关系变成可响应的,哦,我们之前的foo是一个字符串,基本数据类型是不具有响应特性的,那么,我们可能需要传递一个对象。

  1. // 改造一下父级组件提供 'foo'
  2. <template>
  3. <div>
  4. <div>{{foo}}</div>
  5. <son></son>
  6. </div>
  7. </template>
  8. <script>
  9. import Son from "./Son";
  10. export default {
  11. name: "parent",
  12. components: { Son },
  13. provide() {
  14. return {
  15. foo: this.foo
  16. };
  17. },
  18. data() {
  19. return {
  20. foo: {
  21. foo: "测试"
  22. },,
  23. };
  24. },
  25. mounted() {
  26. console.log(this.foo)
  27. },
  28. };
  29. </script>

在这个栗子中,改造了一下父组件提供的数据,测试发现,foo变成了一组可响应的数据。经过尝试我发现在这种情况下如果在孙组件改变inject中foo的值,也会响应的更新到父组件中,当然为了保护单向数据流机制,最佳实践还是不要在子组件里更改inject。

默认值设置

在 2.5.0+ 的注入可以通过设置默认值使其变成可选项

  1. <script>
  2. export default {
  3. name: "grandSon",
  4. inject: {
  5. foo: {
  6. from: 'bar',
  7. default: () => [1, 2, 3]
  8. }
  9. },
  10. mounted() {
  11. console.log(this.foo)
  12. },
  13. };
  14. </script>:

在2.5.0 版本之后的版本中可以为inject提供默认值了,如果它需要从一个不同名字的 property 注入,则使用 from 来表示其源 property,与 prop 的默认值类似,需要对非原始值使用一个工厂方法。

缺点

provide/inject 的缺点还是非常明显的:

  • 当多个后代组件同时依赖同一个父组件提供数据时,只要任一组件对数据进行了修改,所有依赖的组件都会受到影响,实际上是增加了耦合度。
  • 任意层级访问使数据追踪变的比较困难,你并不能准确的定位到是哪一个层级对数据进行了改变,当数据出现问题时,尤其是多人协作时,可能会大大增加问题定位的损耗。

    优点

    在进行组件库或高级插件开发时,provide/inject 仍然是一个不错的选择。

    ref/$refs

    ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

子传父

自定义事件($emit)

子组件

  1. this.$emit('refresh')

父组件

  1. <template>
  2. <!-- 打标签 -->
  3. <add-labels ref="addLabels" @refresh="refresh"></add-labels>
  4. </template>
  5. <script>
  6. export default {
  7. methods:{
  8. // 刷新数据
  9. refresh() {
  10. this.getList()
  11. },
  12. }
  13. }
  14. </script>

父子通信

$parent / $children

$parent/$children的常用场景:封装嵌套组件时,直接使用长辈或者子孙组件的方法,该方法并不改变数据,常常结合$broadcast/$dispatch使用。

  • 组件本身没有父组件和子组件,而是在运行时,每个组件实例的位置,决定其父组件和子组件
  • 每个组件实例有且只有一个父组件(顶级组件实例没有哈)
  • 但每个组件实例可能没有子组件,可能有一个,也可能有多个,所以一般children是数组,没有的时候是空数组
  • js 运行时,若添加或者删除组件实例,那些发生位置变化的组件实例们,父组件和子组件也会发生变化。

写一个页面组件,里面放些组件,打印下$parent/$children
image.png

  1. <template lang="pug">
  2. //- 页面组件
  3. div
  4. list-item
  5. list-item
  6. </template>
  7. <script>
  8. import ListItem from "@/components/ListItem";
  9. export default {
  10. name: "List",
  11. components: { ListItem },
  12. mounted() {
  13. console.log("页面的$parent", this.$parent);
  14. console.log("页面的$children", this.$children);
  15. }
  16. };
  17. </script>

其实还能看到,渲染的时候先子组件的mounted,然后再是自己的mounted(这与生命周期有关).
要是想在子组件里获取父组件的元素之类的,必须使用nextTick。

$broadcast/$dispatch

在使用element-ui时,如下:

  1. <template lang="pug">
  2. el-form
  3. el-form-item
  4. el-input
  5. </template>

假设el-input想要执行el-form上的方法,就会这样this.$parent.$parent.methodXx(),更多层级可能更复杂,于是$dispatch就诞生了。(vuex中使用的dispatch和这个类似)

  1. // main.js
  2. // 向上某个组件,派发事件
  3. Vue.prototype.$dispatch = function(eventName, componentName, ...args) {
  4. let parent = this.$parent;
  5. while (parent) {
  6. // 只有是特定组件,才会触发事件。而不会一直往上,一直触发
  7. const isSpecialComponent = parent.$options.name === componentName;
  8. if (isSpecialComponent) {
  9. // 触发了,就终止循环
  10. parent.$emit(eventName, ...args);
  11. return;
  12. }
  13. parent = parent.$parent;
  14. }
  15. };

这样在el-input里想要触发el-form里的方法,this.$dispatch(‘changeSort’,’el-form’,{isAsc:true})

同理,假设el-form想要执行el-input上的方法,就会这样this.$children[0].$children[0].methodXx(),更多层级可能更复杂,于是$broadcast就诞生了,使用的时候this.$broadcast(‘changeValue’,’el-input’,’hello’)。

注意,$children 是数组,所以当只有一个子组件时,使用[0]获取。当有多个子组件时,它并不保证顺序,也不是响应式的。

  1. // 向下通知某个组件,触发事件
  2. Vue.prototype.$broadcast = function(eventName, componentName, ...args) {
  3. // 这里children是所有子组件,是子组件不是后代组件哈
  4. let children = this.$children;
  5. broadcast(children);
  6. // 这里注意,抽离新的方法递归,而不是递归$broadcast
  7. function broadcast(children) {
  8. for (let i = 0; i < children.length; i++) {
  9. let child = children[i];
  10. const isSpecialComponent = child.$options.name === componentName;
  11. if (isSpecialComponent) {
  12. // 触发了,就终止循环
  13. child.$emit(eventName, ...args);
  14. return;
  15. }
  16. // 没触发的话,就看下有没有子组件,接着递归
  17. child.$children.length &&
  18. child.$broadcast(eventName, componentName, ...args);
  19. }
  20. }
  21. };

$attrs / $listeners(属性/方法)

$attrs的使用

官方定义:
包含了父作用域中不作为prop 被识别 (且获取) 的特性绑定 (class和 style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class和 style除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。
(好抽象一脸懵逼。。。)
$attrs只代表的是那些没有被声明为props的属性,如果某个prop被子组件中声明了(就是这个属性已经在子组件的props中了),在子组件中的$attr会把声明的prop剔除。
个人理解: 一个组件在父组件中被引用,$attrs就是组件标签上的静态属性值(attr)和动态属性值(:attr)的对象集合,这个集合不包含class, style和事件属性

  1. // 父组件
  2. <child-com class="com" name="attr" :foo="foo" :boo="boo" :coo="coo" :doo="doo" @click="test"></child-com>
  3. ...
  4. data() {
  5. return {
  6. foo: 'Hello World!',
  7. boo: 'Hello Javascript!',
  8. coo: 'Hello Vue',
  9. doo: 'Last'
  10. }
  11. },
  12. ...
  13. // child-com.vue
  14. export default {
  15. name: 'childCom',
  16. components: {
  17. childCom2
  18. },
  19. created() {
  20. console.log(this.$attrs) // {name: "attr", foo: "Hello World!", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}
  21. }
  22. }

如果子组件声明了$prop,$attrs中与$props相同的属性会被移除

  1. // child-com.vue
  2. export default {
  3. name: 'childCom',
  4. props: ['foo'], // foo被声明
  5. components: {
  6. childCom2
  7. },
  8. created() {
  9. console.log(this.$attrs) // {name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}
  10. }
  11. }

如果childCom里的子组件还用到foo,可以继续将foo传给子组件

  1. <template>
  2. <div>
  3. <p>foo: {{foo}} </p>
  4. <child-com2 :foo="foo" v-bind="$attrs"></child-com2>
  5. </div>
  6. </template>
  7. <script>
  8. const childCom2 = () => import('./childCom2.vue')
  9. export default {
  10. name: 'childCom1',
  11. props: ['foo'], // foo作为props属性绑定
  12. inheritAttrs: false,
  13. components: {
  14. childCom2
  15. },
  16. created() {
  17. console.log(this.$attrs) // {name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}
  18. }
  19. }
  20. </script>
  21. //childCom2.vue
  22. created() {
  23. console.log(this.$attrs) // {foo: "Hello World!", name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}
  24. }

inheritAttrs要设置成false还是true(默认)看下图就知道,(inheritAttrs为true时允许动态或静态属性显示,为false时即使你给组件设置了属性也不会显示)
image.png
与$props比较
$props必须在组件中注册了props才能用拿到值,所以在嵌套层级比较深的组件中$attrs拿值更加便捷

$listeners的使用

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on=”$listeners”传入内部组件。
归纳起来也是两点:

$listeners是组件的内置属性,它的值是父组件(不含.native修饰器的) v-on事件监听器。
组件可以通 过在自己的子组件上使用v-on=”$listeners”,进一步把值传给自己的子组件。如果子组件已经绑定$listener中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。
父组件:

  1. <template>
  2. <div class="test">
  3. <child v-bind="{name, sex, age}" v-on="{changeName,changeAge}"></child>
  4. </div>
  5. </template>
  6. <script>
  7. import child from './child'
  8. export default {
  9. data() {
  10. return {
  11. name: '张三',
  12. sex: '男',
  13. age: 11,
  14. }
  15. },
  16. components: {
  17. child
  18. },
  19. methods: {
  20. changeName(name) {
  21. this.name = name
  22. },
  23. changeAge(age) {
  24. this.age = age
  25. }
  26. }
  27. }
  28. </script>

子组件child.vue

  1. <template>
  2. <div class="child">
  3. child组件的$attrs {{$attrs}}
  4. <child-child v-bind="$attrs" v-on="$listeners" @showAttrs="showAttrs"></child-child>
  5. </div>
  6. </template>
  7. <script>
  8. import childChild from './child-child'
  9. export default {
  10. name: "child",
  11. props: ['name'],
  12. inheritAttrs: false,
  13. created() {
  14. console.log('child', this.$listeners)
  15. },
  16. components: {
  17. childChild
  18. },
  19. methods: {
  20. showAttrs() {
  21. console.log(this.$attrs)
  22. }
  23. }
  24. }
  25. </script>

孙子组件:child-child.vue

  1. <template>
  2. <div class="child-child">
  3. child-child组件的$attrs {{$attrs}}
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: "child-child",
  9. inheritAttrs: false,
  10. created() {
  11. console.log('child-child',this.$listeners)
  12. }
  13. }
  14. </script>

打印结果:
image.png

全局事件总线

  1. 一种组件间通信的方式,适用于任意组件间通信。
  2. 安装全局事件总线:

    1. new Vue({
    2. ......
    3. beforeCreate() {
    4. Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
    5. },
    6. ......
    7. })
  3. 使用事件总线:

1.接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。

  1. methods(){
  2. demo(data){......}
  3. }
  4. ......
  5. mounted() {
  6. this.$bus.$on('xxxx',this.demo)
  7. }

2.提供数据:this.$bus.$emit(‘xxxx’,数据)

  1. 最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
    1. beforeDestroy() {
    2.         this.$bus.$off('hello')
    3.       },
    例子 : ```javascript //引入Vue import Vue from ‘vue’ //引入App import App from ‘./App.vue’ //关闭Vue的生产提示 Vue.config.productionTip = false

// 安装全局事件总线 Vue.prototype.$bus = new Vue()

//创建vm new Vue({ el:’#app’, render: h => h(App), })

  1. 两个兄弟组件 :<br />**发送的数据的组件:**
  2. ```javascript
  3. <template>
  4. <div class="student">
  5. <h2>学生姓名:{{name}}</h2>
  6. <h2>学生性别:{{sex}}</h2>
  7. <button @click="sendStudentName">把学生名给School组件</button>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. name:'Student',
  13. data() {
  14. return {
  15. name:'张三',
  16. sex:'男',
  17. }
  18. },
  19. mounted() {
  20. // console.log('Student',this.x)
  21. },
  22. methods: {       // 点击事件,把学生们传递到兄弟组件School
  23. sendStudentName(){
  24. this.$bus.$emit('hello',this.name)
  25. }
  26. },
  27. }
  28. </script>

接受数据的组件

  1. <template>
  2. <div class="school">
  3. <h2>学校名称:{{name}}</h2>
  4. <h2>学校地址:{{address}}</h2>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name:'School',
  10. data() {
  11. return {
  12. name:'尚硅谷',
  13. address:'北京',
  14. }
  15. },
  16. mounted() {       //接受数据
  17. this.$bus.$on('hello',(data)=>{
  18. console.log('我是School组件,收到了数据',data)
  19. })
  20. },
  21. beforeDestroy() {       // 销毁调全局事件总线
  22. this.$bus.$off('hello')
  23. },
  24. }
  25. </script>

vuex

核心流程及主要功能

● Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
● 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
● 然后 Mutations 就去改变(Mutate)State 中的数据;
● 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。

各模块在核心流程中的主要功能

● Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
● dispatch∶操作行为触发方法,是唯一能执行action的方法。
● actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
● commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
● mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。
● state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,并进行状态更新。
● getters∶ state对象读取方法。

总结

Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。