pocket-manual icon indicating copy to clipboard operation
pocket-manual copied to clipboard

Vue 组件之间的通信

Open FishPlusOrange opened this issue 7 years ago • 0 comments
trafficstars

Vue 组件之间的通信是 Vue 一大重点知识,这里在不涉及 Vuex 的前提下,对其进行一个总结。

Vue 组件之间的通信大致可以分为以下几种情况:

  • 父子组件之间
    • props
    • ref
    • v-on / $emit
    • v-model
    • $children
    • $parent
    • $listeners
    • .sync
    • provide / inject
  • 兄弟组件之间
    • Event Bus
    • $parent.$children

以下结合相关代码对这几种情况进行讲解。

父子组件之间

ParentComponent.vueChildComponent.vue 为例,假设它们是父子组件:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent'
}
</script>

我们可以通过以下方式实现通信:

  • props

父组件可以通过 props 向子组件传递数据。我们可以在组件上注册一些自定义 props。当一个值传递给一个 prop 时,它就变成了该组件实例的一个属性。

子组件的 props 属性能够接收来自父组件的数据。这样我们就能够在子组件中通过 props 属性获取来自父组件的数据,其访问方式和 data 属性一致。

相关代码如下:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component :msg="msg"/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: ['msg'],
  mounted () {
    console.log(this.msg) // 来自 ParentComponent 的文本
  }
}
</script>

但是,需要注意的是,父子组件 props 之间是单向下行绑定的。即通过 props 只能从父组件向子组件传递数据,父组件 props 的更新会向下流动到子组件中,反之不行。这样可以防止从子组件意外改变父组件的状态,从而导致应用的数据流向难以理解。

props 主要用于向子组件传递数据,使用场景比如在新建一个子组件时传递一些自定义的数据。

  • ref

ref 被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的 $refs 对象上。

ref 可以理解为索引,如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

所以我们可以在子组件上使用 ref,通过父组件的 $refs 对象获取子组件中定义的属性和方法:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component ref="childComponent"/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  mounted () {
    console.log(this.$refs.childComponent.msg) // 来自 ChildComponent 的文本
    this.$refs.childComponent.showMsg() // 来自 ChildComponent 的文本
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  data () {
    return {
      msg: '来自 ChildComponent 的文本'
    }
  },
  methods: {
    showMsg () {
      console.log(this.msg)
    }
  }
}
</script>

但是,需要注意的是,ref 本身是作为渲染结果被创建的,$refs 只会在组件渲染完成之后生效,并且 $refs 不是响应式的。所以我们应该避免在模板或计算属性中使用 $refs

ref 主要用于调用子组件的属性和方法,使用场景比如在父组件数据变化后调用子组件的方法以更新子组件的状态,父子路由同样适用。此外,ref 更常在普通的 DOM 元素上使用,类似于选择器的作用。

  • v-on / $emit

子组件到父组件之间的通信可以通过事件实现,即 v-on$emit

以下面代码为例,在子组件中, $emit 绑定了一个自定义事件 emitShowMsg,当 $emit 被执行时,就会将第二个参数数据传递给父组件,父组件通过 v-on:emitShowMsg 监听并执行自定义方法 showMsg 从而接收到来自子组件的数据:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component @emitShowMsg="showMsg"/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  methods: {
    showMsg (emitMsg) {
      console.log(emitMsg) // 来自 ChildComponent 的文本
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  mounted () {
    this.$emit('emitShowMsg', '来自 ChildComponent 的文本')
  }
}
</script>
  • v-model

props 和事件还可以使用语法糖 v-model 实现。

在使用时,v-model 默认解析成名为 valueprop 和名为 input 的事件,实现双向绑定:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component v-model="msg"/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  },
  updated () {
    console.log(this.msg) // 来自 ChildComponent 的文本
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: ['value'],
  mounted () {
    console.log(this.value) // 来自 ParentComponent 的文本
    this.$emit('input', '来自 ChildComponent 的文本')
  }
}
</script>

既然 v-modelprops 和事件的语法糖,那么我们就可以通过 v-on 监听相关事件:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component
      v-model="msg"
      @input="showMsg"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  },
  methods: {
    showMsg (emitMsg) {
      console.log(emitMsg) // 来自 ChildComponent 的文本
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: ['value'],
  mounted () {
    console.log(this.value) // 来自 ParentComponent 的文本
    this.$emit('input', '来自 ChildComponent 的文本')
  }
}
</script>

我们还可以通过 model 属性自定义 v-model 相对应的 prop 和事件名(以下省略了部分代码):

<!-- ChildComponent.vue -->
<script>
export default {
  name: 'ChildComponent',
  model: {
    prop: 'msg',
    event: 'change'
  },
  props: ['msg'],
  mounted () {
    console.log(this.msg)
    this.$emit('change', '来自 ChildComponent 的文本')
  }
}
</script>

语法糖 v-model 常用于父子组件之间的单属性通信,如 控制 UI 组件的显示隐藏。

  • $children

父组件可以通过 $children 来访问直接子组件实例的属性和方法。

$children 返回一个 Vue 组件实例数组,我们需要知道对应子组件实例在数组中的位置,通过数组下标进行访问;也可以通过组件 name 查询到需要的组件实例:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  mounted () {
    console.log(this.$children[0].$options.name) // ChildComponent
    console.log(this.$children[0].msg) // 来自 ChildComponent 的文本
    this.$children[0].showMsg() // 来自 ChildComponent 的文本
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  data () {
    return {
      msg: '来自 ChildComponent 的文本'
    }
  },
  methods: {
    showMsg () {
      console.log(this.msg)
    }
  }
}
</script>

由于 $children 访问的是直接子组件实例,并且需要知道目标子组件在 $children 数组中的位置;如果是通过组件 name 进行查询,还需要编写对应的匹配函数,所以不适用于组件层级复杂的情况。

  • $parent

子组件可以通过 $parent 来访问直接父组件实例的属性和方法。

$children 不同的是,$parent 直接返回一个 Vue 组件实例:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  },
  methods: {
    showMsg () {
      console.log(this.msg)
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  mounted () {
    console.log(this.$parent)
    console.log(this.$parent.$options.name) // ParentComponent
    console.log(this.$parent.msg) // 来自 ParentComponent 的文本
    this.$parent.showMsg() // 来自 ParentComponent 的文本
  }
}
</script>

由于 $parent 访问的也是直接父组件实例,所以也不适用于组件层级复杂的情况。

  • $listeners

子组件可以通过 $listeners 访问父组件的绑定事件监听器,即通过 v-on 绑定在该子组件上的所有事件。

$listeners 返回一个对象。其中,属性名为事件名,属性值为对应的回调函数:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component
      @event-1st="showMsg"
      @event-2nd="showMsg"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  },
  methods: {
    showMsg () {
      console.log(this.msg)
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  mounted () {
    this.$listeners['event-1st']() // 来自 ParentComponent 的文本
    this.$listeners['event-2nd']() // 来自 ParentComponent 的文本
  }
}
</script>

需要注意的是,$listeners 不包含 .native 修饰的事件。

  • .sync

.sync 修饰符其实也是一个语法糖,可以进行父子组件之间的通信,实现双向绑定:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component :msg.sync="msg"/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  data () {
    return {
      msg: '来自 ParentComponent 的文本'
    }
  },
  updated () {
    console.log(this.msg) // 来自 ChildComponent 的文本
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: ['msg'],
  mounted () {
    console.log(this.msg) // 来自 ParentComponent 的文本
    this.$emit('update:msg', '来自 ChildComponent 的文本')
  }
}
</script>

ParentComponent.vue 中:

<child-component :msg.sync="msg"/>

实际上被解析成:

<child-component
  :msg="msg"
  @update:msg="msg = $event"
/>

相关使用注意事项参考官方文档

  • provide / inject

除了以上方法,我们还可以通过 provideinject 实现父子组件之间的通信:

<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>ParentComponent.vue</h1>
    <child-component/>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent'
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  provide: {
    msg: '来自 ParentComponent 的文本'
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>ChildComponent.vue</h3>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  inject: ['msg'],
  mounted () {
    console.log(this.msg) // 来自 ParentComponent 的文本
  }
}
</script>

其实,provideinject 支持跨多层级父子组件之间的通信。

相关使用注意事项参考官方文档

兄弟组件之间

BrotherComponent1st.vueBrotherComponent2nd.vue 为例,假设它们是兄弟组件:

<!-- BrotherComponent1st.vue -->
<template>
  <div>
    <h1>BrotherComponent1st.vue</h1>
  </div>
</template>
<!-- BrotherComponent2nd.vue -->
<template>
  <div>
    <h1>BrotherComponent2nd.vue</h1>
  </div>
</template>

我们可以通过以下方式实现通信:

  • Event Bus

兄弟组件之间的通信可以依赖一个中介,这个中介我们通常称之为 Event Bus,这里我们把它命名为 bus.js。其暴露一个 Vue 实例,这个实例就承担起了兄弟组件之间通信的桥梁。

相关代码如下:

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

在兄弟组件中导入:

import Bus from './bus.js'

发送方 BrotherComponent1st 使用 Bus.$emit(eventName, [...args]) 发送数据,接收方使用 Bus.$on(eventName, callback) 接收数据:

<!-- BrotherComponent1st.vue -->
<template>
  <div>
    <h1>BrotherComponent1st.vue</h1>
  </div>
</template>

<script>
import Bus from './bus.js'
export default {
  name: 'BrotherComponent1st',
  methods: {
    sendMsg () {
      Bus.$emit('showMsg', '来自 BrotherComponent1st 的文本')
    }
  }
}
</script>
<!-- BrotherComponent2nd.vue -->
<template>
  <div>
    <h1>BrotherComponent2nd.vue</h1>
  </div>
</template>

<script>
import Bus from './bus.js'
export default {
  name: 'BrotherComponent2nd',
  methods: {
    getMsg () {
      Bus.$on('showMsg', emitMsg =>
        console.log(emitMsg)) // 来自 BrotherComponent1st 的文本
    }
  }
}
</script>

其实,我们可以通过 Event Bus 实现任意组件之间的通信。

  • $parent.$children

通过 $parent.$children,我们可以得到一个组件的兄弟组件实例数组。

具体使用方法和注意事项,与上文父子组件之间的通信中所提到的 $children 方式一致,此处不再赘述。

以上基于 Vue2.x

FishPlusOrange avatar Jun 26 '18 13:06 FishPlusOrange