vue父子组件通信的几种方式

206 阅读2分钟

本文都将采用Vue3的写法来演示。

props和emits

props

传递静态或动态的 Prop

<blog-post title="My journey with Vue"></blog-post>
<blog-post :title="post.title"></blog-post>

如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind (用 v-bind 代替 :prop-name)。例如,对于一个给定的对象 post

post: {
  id: 1,
  title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

单向数据流 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态,且 Vue 无法为此向你发出警告。作为一个通用规则,应该避免修改任何 prop,包括对象和数组,因为这种做法无视了单向数据绑定,且可能会导致意料之外的结果。

这里有两种常见的试图变更一个 prop 的情形:

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
  • 这个 prop 以一种原始的值传入且需要进行转换。
props: ['initialCounter', 'size'],
data() {
  return {
    counter: this.initialCounter
  }
}
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}

emits

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

注意:强烈建议使用 emits 记录每个组件所触发的所有事件。这尤为重要,因为我们移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。

ref和$parent

父组件访问子组件实例

尽管存在 prop 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为此,可以使用 ref attribute 为子组件或 HTML 元素指定引用 ID。

在 composition API 中,使用 template refs,在 setup 中创建 ref 对象返回,在 template 中添加同名的 ref 属性。

<template> 
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM 元素将在初始渲染后分配给 ref
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

Provide/Inject

Provide

在 setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 来定义每个 property。provide 函数允许你通过两个参数定义 property:name和value。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>

inject

在 setup() 中使用 inject 时,也需要从 vue 显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。

<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

响应性

为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用ref或reactive]。使用 MyMap 组件,我们的代码可以更新如下:

setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }

现在,如果这两个 property 中有任何更改,MyMarker 组件也将自动更新!

修改响应式 property

当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部

setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)

    return {
      location
    }
  },
  methods: {
    updateLocation() {
      this.location = 'South Pole'
    }
  }

然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。

const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation)

最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly

provide('location', readonly(location))
provide('geolocation', readonly(geolocation))

事件总线

有一些特殊的情形,我们需要使用事件监听器完成父子通讯。比如:父组件中有 slot,子组件是以 slot 形式存在的,没法添加 ref。

在 2.x 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ($on$off 和 $once)。这可以用于创建一个事件总线,以创建在整个应用中可用的全局事件监听器:

// eventBus.js
const eventBus = new Vue()
export default eventBus

// ChildComponent.vue
import eventBus from './eventBus'

export default {
  mounted() {
    // 添加 eventBus 监听器
    eventBus.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // 移除 eventBus 监听器
    eventBus.$off('custom-event')
  }
}

// ParentComponent.vue
import eventBus from './eventBus'

export default {
  methods: {
    callGlobalCustomEvent() {
      eventBus.$emit('custom-event') // 当 ChildComponent 已被挂载时,控制台中将显示一条消息
    }
  }
}

在vue3中,我们从实例中完全移除了 $on$off 和 $once 方法。事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或 tiny-emitter

// eventBus.js
import emitter from 'tiny-emitter/instance'

export default {
  $on: (...args) => emitter.on(...args),
  $once: (...args) => emitter.once(...args),
  $off: (...args) => emitter.off(...args),
  $emit: (...args) => emitter.emit(...args),
}

它提供了与 Vue 2 相同的事件触发器 API。在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。

$attrs

官方描述:包含了父作用域中不作为组件props或自定义事件的 attribute 绑定和事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件——这在创建高阶的组件时会非常有用。

在vue2中有attrsattrs和listeners,但是在vue3中只有attrs,也就是vue3中的attrs,也就是vue3中的attrs包含了vue2中的attrsattrs和listeners。

$attrs的作用可以类似于props和emits的候补

比如A组件向B组件传递了很多props和方法,但是B组件中的props和emits只写了其中的一个,那么其他的属性和方法就都放在attrs里面。如果B组件向把attrs里面。如果B组件向把attrs向下传递,那么只需要这么写:v-bind="$attrs"。

注意:多余的属性可能会放在组件的根标签作为标签的属性存在,如果不想这样可是设置inheritAttrs: false。