原稿:语雀 · 组件通信 · 原目录:Vue › Vue 知识点 › 组件通信

父子通信

【父 → 子】props

子组件设置 props,父组件通过 v-bind 向子组件传递数据

【子 → 父】vm.$emit

子组件通过 $emit 触发子实例的事件,父组件使用 v-on 监听子实例事件并指定回调函数,通过 $emit 传递的参数可以使用 $event访问,或被父组件回调接收

下面这两种通信方法都是为了实现父子组件的双向绑定,尽管不推荐从子组件 prop 修改父组件数据,打破单向数据流,但是某些情形确实需要在子组件中触发对父组件数据的修改。下面两种方法都是触发事件,抛出目标值给父组件,父组件在回调中修改自身的 prop,在不打破单向数据流的情况下告知父组件要修改的信息

【双向绑定】v-model

这里的 v-model 专门指组件上使用的,一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,可以通过在子组件设置 model 改变默认的 prop和 event

如下,在 model:{} 中修改了默认绑定的 prop 与 event,在子组件中监听了 change 事件,当触发子组件 change 时,子组件又会触发自定义事件 change(两个 change 不同)

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)">`
})

父实例中使用 v-model,自定义组件的 v-model 是 v-bind + v-on 的语法糖,完整写法如下,使用 $event 即可获得子组件抛出的值

<base-checkbox v-model="lovingVue"></base-checkbox>
<!-- 等价于 -->
<base-checkbox :checked="lovingVue"  @change="lovingVue=$event" ></base-checkbox>

【双向绑定】.sync

.sync 修饰符本质和 v-model 类似,都是复合操作的简写。官方推荐的做法是使用 update:myPropName 模式:

当子组件有 title prop 时,子组件通过 $emit 抛出新值,触发自定义事件 update:XXX,XXX 可代表 props 中的任一属性

this.$emit('update:title', newTitle)

父组件监听 update:XXX,在回调中接受值并修改自身 prop,同时该属性又会通过 v-bind 传递回子组件 props

<text-document v-bind:title.sync="doc.title"></text-document>
<!-- 等价于 -->
<text-document :title="doc.title" @update:title="doc.title=$event"> </text-document>

v-model 和 .sync 对比

二者本质完全相同,都是 v-bind 与 v-on 的语法糖,细微差别

  • 语义上,v-model 绑定的是表单控件的 value,比如 <input><select> ,些组件一般都带有原生的 value 属性对应表单值,因此 v-model 默认有 value 与 input 事件
  • .sync 更加灵活,它可以给多个 prop 使用,如下就可以把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。而 v-model 在一个组件中只能有一个
<text-document v-bind.sync="doc"></text-document>

【父 → 子】vm.$refs

尽管存在 prop 和事件,有时仍需要直接访问一个子组件。通过 ref attribute 可以为子组件赋予一个 ID 引用

<base-input ref="usernameInput"></base-input>

之后在定义了该 ref 的父组件中,可以使用 this.$refs 访问子组件实例。

this.$refs.usernameInput

使用 ref 需要注意

  • v-for 和 ref 一起使用时:当有 v-for="item in list" 时,this.$refs.ID 访问到的并不是每一个 item,而是整个 list 数组
  • $refs 是作为渲染结果被创建的,所以在初始渲染的时候它还不存在,此时无法无法访问
  • $refs 不是响应式的,只能拿到获取它的那一刻子组件实例的状态,所以要避免在模板和计算属性中使用它

【子 → 父】vm.$parent

$parent 属性可以用来从一个子组件访问父组件的实例

  • 特点:使用 $parent 比使用 prop 传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop 那样需要提前定义好
  • 缺点:使用 $parent 会导致父组件数据变更后很难去定位这个变更是从哪里发起的,所以在绝大多数情况下不推荐使用。

在有些场景下,两个组件之间可能是父子关系,也可能是更多层嵌套的祖孙关系,这时就可以使用 $parent 向上获取,如下面是 Element UI 中 el-radio 源码,该组件即使用 $parent 获取父组件 el-radio-group 的数据

// el-radio组件
let parent = this.$parent;
while (parent) {
  if (parent.$options.componentName !== 'ElRadioGroup') {
    parent = parent.$parent;
  } else {
    this._radioGroup = parent; // this._radioGroup 为组件 el-radio-group 的实例
  }
}

【父 → 子】vm.$children

$children 属性可以获取当前实例的直接子组件。$children 并不保证顺序,也不是响应式的。如果使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源


非父子通信

【跨级通信】\(attrs</a> 和 <a href="https://cn.vuejs.org/v2/api/#vm-listeners" class="ne-link" target="_blank" rel="noopener">\)listeners

当要和一个嵌套很深的组件进行通信时,如果使用 prop 和 events 就会显的十分繁琐,中间的组件只起到了一个中转站的作用,要传递的数据需要在中间的组件重复写很多遍,反过来从后代组件向祖先组件使用 events 传递也会有同样的问题

<!--父组件-->
<parent-component :message="message">我是父组件</parent-component>
  <!--子组件-->
  <child-component :message="message">我是子组件</child-component>
    <!--孙子组件-->
    <grand-child-component :message="message">我是孙子组件</grand-child-component>

使用 $attrs 和 $listeners 就可以简化这样的写法:

  • $attrs 会包含父组件中没有被 prop 接收的所有属性(不包含class 和 style 属性),可以通过 v-bind="$attrs" 直接将这些属性传入内部组件
  • $listeners 会包含所有父组件中的 v-on 事件监听器 (不包含 .native 修饰器的) ,可以通过 v-on="$listeners" 传入内部组件
<!--父组件 parent.vue-->
<template>
    <child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
    inheritAttrs: false,
    data() {
        return {
            name: '通信',
            message: 'Hi',
        }
    },
    methods: {
        sayHello(mes) {
            console.log('mes', mes) // => "hello"
        },
    },
}
</script>

child.vue 的 $attrs 包含了其父组件传递的除了 name 外的数据; $listeners 则包含了其父组件的传来的监听,并作为中转将数据和方法传递给孙组件

<!--子组件 child.vue-->
<template>
    <grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
    data() {
        return {}
    },
    props: {
        name,
    },
}
</script>
<!--孙子组件 grand-child.vue-->
<template>
</template>
<script>
export default {
    created() {
        this.$emit('sayHello', 'hello')
    },
}
</script>

【跨级通信】$root

通过 $root,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data,就可以实现组件间的数据共享。缺点同 $parent,无法确认变更的发起者,增加调试难度

// 根组件 data
data() {
  return {
    author: ''
 }
}
<!--组件A-->
<script>
export default {
    created() {
        this.$root.author = '于是乎'
    }
}
</script>
<!--组件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>

【跨级通信】provide 和 inject

provide 和 inject 需要在一起使用,它可以使一个祖先组件向其所有子孙后代注入一个依赖,可以指定想要提供给后代组件的数据/方法,不论组件层次有多深,都能够使用

<!--祖先组件-->
<script>
export default {
    // 对象or返回对象的函数:接收kv对
    provide: {
        author: 'yushihu',
    },
    data() {},
}
</script>
<!--子孙组件-->
<script>
export default {
        // 字符串数组or对象
    inject: ['author'],
    created() {
        console.log(this.author) // => yushihu
    },
}
</script>

provide 和 inject 绑定不是响应的,它被设计是为组件库和高阶组件服务的,平常业务中的代码不建议使用

【任意两组件】eventBus

在要相互通信的两个组件中,都引入同一个新的 vue 实例,然后在两个组件中通过分别调用这个实例的事件触发和监听来实现通信

//eventBus.js
import Vue from 'vue';
export default new Vue();
<!--组件A-->
<script>
import Bus from 'eventBus.js';
export default {
    methods: {
        sayHello() {
            Bus.$emit('sayHello', 'hello');
        }
    }
}
</script>
<!--组件B-->
<script>
import Bus from 'eventBus.js';
export default {
    created() {
        Bus.$on('sayHello', target => {
            console.log(target);  // => 'hello'
        });
    }
}
</script>

【状态管理】Vuex

Vuex 采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

store

仓库,用来保存应用的状态(全局变量)

state

状态,类似全局变量,保存着应用或模块中唯一的信息

mutation

突变事件,是唯一改变 store.state 的途径,但不允许发生异步操作

action

类似 mutation,但 action 需要 commit mutation ,可以产生异步回调

getter

可理解为 state 的计算属性

module

可以把 store 分给不同的module,每个 module 有独自的 state,mutation 等

下面的 store.js 简单实现了状态管理

//store.js
var store = {
  debug: true,
  state: {
    author: 'yushihu!'
  },
  setAuthorAction (newValue) {
    if (this.debug) console.log('setAuthorAction triggered with', newValue)
    this.state.author = newValue
  },
  deleteAuthorAction () {
    if (this.debug) console.log('deleteAuthorAction triggered')
    this.state.author = ''
  }
}

小结(10种)

父 → 子
props:父实例中使用 v-bind:a_prop 传递数据,子组件设置 props:['a_prop']
$refs:通过 ref='a_ref' 注册子组件,使用 this.$refs.a_ref 访问子组件,非响应式
$children:获取当前实例的直接子组件,非响应式

子 → 父
$emit:子组件使用 $emit('change', $event.target.checked) 触发父组件监听事件,并传递参数
$parent:子组件使用 this.$parent 获取父组件实例,配合 $options 获取父组件初始化选项

双向绑定
v-model:实际为 <:value="XXX" @input="XXX=$event"> 缩写
.sync:实际为 <:title="doc.title" @update:title="doc.title=$event"> 缩写

跨级通信
$attrs/$listeners,$root,provide+inject

两组件间
eventBus

状态管理
Vuex