Leiy
  • 700

Vue 组件间通信 12 种方法汇总

 阅读约 22 分钟

自检清单.png

补充一个 slot 传值。

父组件向子组件传值

1. 通过属性传值 props

props 可以是数组或对象,用于接收来自父组件的数据。

// 父组件 List.vue
<template>
  <div>
    <List-item :str="str" :obj="obj" :arr="arr"></List-item>
  </div>
</template>
<script>
import ListItem from "./ListItem";
export default {
  data() {
    return {
      str: "给子组件传值",
      obj: {msg: "给子组件传值"},
      arr: [1, 2, 3]
    }
  },
  components: {
    ListItem
  }
}
</script>

// 子组件 ListItem.vue
<template>
  <div>
    <div>{{msg}}</div>
    <div>{{obj}}</div>
    <div>{{arr}}</div>
  </div>
</template>
<script>
export default {
  props: {
    msg: String, // props是字符串
    obj: Object, // props是对象
    arr: Array   // props是数组
  }
}
</script>

2. 使用修饰符 .sync

修饰符 .sync2.3.0+ 新增,它对 props 起到了一种修饰的作用,使用 .sync 进行修饰的 props 意味子组件有修改它的意图,这种情况下它只起到一个标注性作用,有它没它都不会影响逻辑(后文会介绍使用 .sync 的其他作用)。

使用 .sync 修改上边的代码:

// 父组件 List.vue
<template>
  <!-- 这里不写 .sync 也不会影响结果 -->
  <List-item :title.sync="title" @update:title="updataTitle"></List-item>
</template>
<script>
import ListItem from "./ListItem";
export default {
  data() {
    return {
      title: "我是title",
    }
  },
  components: {
    ListItem
  },
  methods: {
   updataTitle(res) {
    this.title = res;
   }
  }
}
</script>

// 子组件 ListItem.vue
<template>
  <div>
    <button @click="handleClick">Click me</button>
    <div>{{title}}</div>
  </div>
</template>
<script>
export default {
  props: {
    title: String, 
  },
  methods: {
   handleClick() {
    // 子组件向父组件传值
    this.$emit('update:title', '我要父组件更新 title');
   }
  }
}
</script>

使用.sync 向子组件传递 多个props:

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 `v-on 监听器。

更多介绍,.sync

3. 通过 $parent 获取父组件实例的方法或者属性

这种方式,从严格意思上讲不是值的传递,而是一种"取"(不推荐直接通过实例进行值的获取)。

可以通过 Vue 的实例属性 $parent 获得父组件的实例,借助实例可以调用父实例中的方法,或者获取父实例上的属性,从而达到取值的目的。

// 父组件 List.vue
...
<script>
export default {
  data() {
    return {
      message: "hello children",
      msg: "hello"
    }
  },
  methods: {
    sendMessage() {
      return this.message;
    }
  }
}
</script>

// 子组件 ListItem.vue
<template>
  <div>
    <div>{{data}}</div>
    <div>{{msg}}</div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      data: "",
      msg: ""
    }
  },
  mounted() {
    this.data = this.$parent.sendMessage(); // 调用父实例中的方法
    this.msg = this.$parent.msg; // 获取父实例中的属性
  }
}
</script>

拓展

子组件调用父组件中的方法:

  • 通过 $parent 获取父实例 this.$parent.event
  • 通过 props 传递方法。
  • 通过 $emit 监听父组件中的方法 this.$emit("envnt")

子组件向父组件传值

1. 通过事件传值 $emit

  • 子组件使用 $emit 发送一个自定义事件,事件名称是一个字符串。
  • 父组件使用指令 v-on 绑定子组件发送的自定义事件。
// 父组件 List.vue
<template>
  <div>
    <!-- 监听自定义事件 -->
    <List-item v-on:welcome="getWelcome"></List-item>
  </div>
</template>
<script>
import ListItem from "./List-item";
export default {
  components: {
    ListItem
  },
  methods: {
    getWelcome(data) {
      alert(data)
    }
  }
}
</script>

// 子组件 ListItem.vue
<template>
  <button @click="handleClick">Click me</button>
</template>
<script>
export default {
  methods: {
    handleClick() {
   // 使用 $emit 发送自定义事件 welcome
      this.$emit('welcome', 'hello');
    }
  }
}
</script>

2. 通过 $children 获取子组件实例

此方式同 $parent,这里就不进行介绍了。

3. 通过 ref 注册子组件引用

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,可以通过 ref 特性为这个子组件赋予一个 ID 引用。

<template>
  <div>
    <List-item ref="item" :title="title"></List-item>
    <div>{{data}}</div>
  </div>
</template>
<script>
import ListItem from "./List-item";
export default {
  data() {
    return {
      title: "我是title",
      data: ""
    }
  },
  components: {
    ListItem
  },
  mounted() {
    this.data = this.$refs.item.message;
  }
}
</script>

更多 ref 用法

兄弟组件传值

1. Bus 中央事件总线

非父子组件传值,可以使用一个空的 Vue 实例作为中央事件总线,结合实例方法 $on$emit 完成传值操作。

Bus 的定义方式有以下三种:

  1. Bus 抽离出来,组件有需要时进行引入。

    // Bus.js
    import Vue from 'vue'
    const Bus = new Vue()
    export default Bus
  2. Bus 挂载到 Vue 根实例的原型上。

    import Vue from 'vue'
    Vue.prototype.$bus = new Vue();
  3. Bus 注入到 Vue 根对象上。

    import Vue from 'vue'
    const Bus = new Vue()
    new Vue({
      el:'#app',
      data: {
        Bus
      }  
    })

下面案例中的 Bus 挂载在 Vue 原型上:

// 组件1 使用 $emit 向外部发布自定义事件
<template>
  <button @click="handleClick"> Send Message</button>
</template>
<script>
export default {
  data() {
    return {
      message: "给兄弟组件传值",
    }
  },
  methods: {
    handleClick() {
      this.$Bus.$emit("sendMessage", this.message)
    }
  }
}
</script>

// 组件2 使用 $on 订阅外部发布的事件
<template>
  <div>
    {{data}}
  </div>
</template>
<script>
export default {
  data() {
    return {
      data: "",
    }
  },
  mounted() {
    this.$Bus.$on("sendMessage", data => {
      this.data = data;
    })
  }
}
</script>

注意:注册的 Bus 要在组件销毁时卸载,否则会多次挂载,造成触发一次但多个响应的情况。

beforeDestroy () {
  this.$Bus.$off('sendMessage', this.message);
}

2. Vuex 状态管理器

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

图片引用自网络:

vuex

Vuex 的具体使用

3. 通过父组件进行过渡

不是方法的方法:

  1. 子组件 A 通过事件 $emit 传值传给父组件。
  2. 父组件通过属性 props 传值给子组件 B

深层次嵌套组件传值

1. 依赖注入 provide/inject

provide 选项允许我们指定我们想要提供给后代组件的数据/方法。

provide: function () {
  return {
    getMap: this.getMap
  }
}

然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性:

inject: ['getMap']

provideinject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root 做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

2. $attrs/inheritAttrs

这个两个属性是 2.4 新增的特性。

$attrs:

官网介绍的很累赘,暂且理解为非 props 属性集合。更多介绍

当一个组件中没有声明任何 prop 时,this.$attrs 可以获取到所有父作用域的属性绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传给其内部组件 —— 在创建高级别的组件时非常有用。

inheritAttrs:

控制元素属性是否显示在 dom 上,默认值为 true

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

祖先组件:

<template>
  <div>
    <List-item :title="title" :message="message"></List-item>
  </div>
</template>
<script>
import ListItem from "./List-item";
export default {
  data() {
    return {
      title: "我是title",
      message: "传给后代"
    }
  },
  components: {
    ListItem
  }
}
</script>

父组件:

<template>
  <div>
    <h1>{{title}}</h1>
    <h2>{{$attrs.message}}</h2>
    <!-- 通过 v-bind="$attrs" 传入后代组件-->
    <ListItem2 v-bind='$attrs'></ListItem2>
  </div>
</template>
<script>
import ListItem2 from './List-item2'
export default {
  props: {
    title: String
  },
  components: {
    ListItem2
  },
  // 默认为 true,如果传入的属性子组件没有 prop 接受,就会以字符串的形式作为标签的属性存在 <div message="传给后代"></div>
  // 设为 false,在 dom 中就看不到这些属性 <div>...</div>
  inheritAttrs: false
}
</script>

后代组件:

<template>
  <div>
    {{$attrs.message}}
  </div>
</template>
<script>
export default {
  mounted() {
    console.log(this.$attrs)    // {message: "传给后代"}
  }
}
</script>

渲染出来的结果为:

$attrs/inheritAttrs

插槽 slot 与子组件传值

在实际项目中确实有遇到插槽后备内容 动态显示的情况,所以这里要补充一下插槽 后备内容 是如何与子组件进行通信的。

插槽后备内容是指:写在父组件中,包含在子组件标签里的,与子组件中的 slot 对应。

<template>
  <child-component>
    我是插槽的后备内容
  </child-component>
</template>

比如这里有一个含有 slotcurrent-user 组件,它的模版结构是这样的:

<!-- 子组件 current-user.vue -->
<template>
  <div>
    <div>current-user组件</div>
    <slot>插槽里默认显示:{{user.firstName}}</slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        firstName: "zhao",
        lastName: "xinglei"
      }
    }
  }
}
</script>

它的父组件是这样的:

<!-- 父组件 Users.vue -->
<template>
  <div>
    <div>我是Users组件</div>
    <current-user>
      我是插槽里的后备内容: {{user.lastName}}(我想显示为子组件中 user.lastName )
    </current-user>
  </div>
</template>

<script>
import CurrentUser from './Current-User.vue'
export default {
  components: {
    CurrentUser
  }
}
</script>

我们看到,在父组件 Users 中,为子组件 current-user 提供的后备内容中,想要显示子组件定义的 user.firstName 是不能做到的。

官网中提供一个指令 v-slot,它与 props 结合使用从而达到插槽后备内容与子组件通信的目的。

我们首先需要在子组件的 slot 中传递一个 props(这个props 叫做插槽props),这里我们起名叫 user

<!-- 子组件 current-user.vue -->
<template>
  <div>
    <div>current-user组件</div>
    <slot :user="user">
      插槽里默认显示:{{user.firstName}}
    </slot>
  </div>
</template>

在父组件中,包含插槽后备内容的子组件标签上我们绑定一个 v-slot 指令,像这样:

<template>
  <div>
    <div>我是Users组件</div>
    <!-- slotProps里的内容就是子组件传递过来的 props -->
    <!-- "user": { "firstName": "zhao", "lastName": "xinglei" } -->
    <current-user v-slot="slotProps">
      {{slotProps}}
    </current-user>
  </div>
</template>

最后渲染出来的结果为:

作用域插槽

官网给这种插槽起名叫做作用域插槽更多了解

总结

1. 组件之间传值无非就是通过属性、事件和操作 Vue 实例进行的。
2. 操作实例进行组件件通信,实例属性 $root、$parent、$children 分别对应了根实例、父实例、子实例。
3  ref 子组件引用,在操作表单元素时会应用的到。
4. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,简单的应用不要使用 Vuex。
5. Vue.observable() 让一个对象可响应,可以作为最小化的跨组件状态存储器(本文未提到)。
阅读 1.4k更新于 2019-11-05

推荐阅读
前端infoQ
用户专栏

前端技术文章

749 人关注
6 篇文章
专栏主页
目录