• vue3 快速入门系列 —— 组件通信


    vue3 快速入门系列 - 组件通信

    组件通信在开发中非常重要,通信就是你给我一点东西,我给你一点东西。

    本篇将分析 vue3 中组件间的通信方式。

    Tip:下文提到的绝大多数通信方式在 vue2 中都有,但是在写法上有一些差异。

    准备环境

    vue3 基础上进行。

    新建三个组件:爷爷、父亲、孩子A、孩子B,在主页 Home.vue 中加载组件Gradfather.vue

    
    <template>
        <p># 爷爷p>
        <hr>
        <Father/>
    template>
    
    <script lang="ts" setup name="App">
    import Father from './Father.vue';
    script>
    
    
    
    
    
    
    
    
    <template>
        <p># 孩子Ap>
    template>
    
    <script lang="ts" setup name="App">
    
    script>
    
    
    <template>
        <p># 孩子Bp>
    template>
    
    <script lang="ts" setup name="App">
    
    script>
    

    浏览器呈现:

    # 爷爷
    ——————————————————
    # 父亲
    ——————————————————
    # 孩子A
    ——————————————————
    # 孩子B
    

    下文将再此基础上演示组件间的通信。

    props

    需求:实现父给子一件新衣服,子给父一个吻,都用 props 实现。

    请看代码:

    
    <template>
        <p># 父亲p>
        <p>来自孩子A: {{ b }}p>
        <hr>
        // 传一个属性和一个方法
        <ChildA :gift="a" :sendWen="getWen"/>
    template>
    
    <script lang="ts" setup name="App">
    import ChildA from '@/views/ChildA.vue'
    
    import {ref} from 'vue'
    let a = ref('新衣服')
    
    let b = ref('')
    
    function getWen(val:string){
        b.value = val
    }
    script>
    
    
    <template>
        <p># 孩子Ap>
        <p>来自父亲:{{ gift }}p>
    template>
    
    <script lang="ts" setup name="App">
    const props = defineProps(['gift', 'sendWen'])
    // 调用方法,通过参数传递数据给父组件
    props.sendWen('kiss')
    script>
    

    页面呈现:

    # 父亲
    
    来自孩子A: kiss
    ————————————————————
    # 孩子A
    
    来自父亲:新衣服
    

    子给父传数据借助了方法。

    通常我们可能会用自定义事件来向父组件传递数据,但是在 react 中,子组件给父组件传递数据就是用 props 传递方法的这种方式进行的。

    Tip:祖父给孙子传递就不要用 props。否则按照这个思路,无论什么情况都可以用这个方法。

    自定义事件

    请看示例:

    
    <template>
        <p># 父亲p>
        <p>来自孩子A: {{ b }}p>
        <hr>
        
        <ChildA :gift="a" @send-gift="getGift"/>
    template>
    
    <script lang="ts" setup name="App">
    import ChildA from '@/views/ChildA.vue'
    
    import {ref} from 'vue'
    let a = ref('新衣服2')
    
    let b = ref('')
    
    function getGift(val:string){
        b.value = val
    }
    script>
    

    父组件通过 @send-gift="getGift" 给孩子绑定自定义事件,子组件通过 defineEmits 声明可以触发的事件,最后通过 emit('send-gift', 'kiss2') 触发事件,并将参数传过去。

    
    <template>
        <p># 孩子Ap>
        <p>来自父亲:{{ gift }}p>
    template>
    
    <script lang="ts" setup name="App">
    defineProps(['gift',])
    // 声明事件 - 定义一个组件可以发射(emit)的事件
    const emit = defineEmits(['send-gift'])
    emit('send-gift', 'kiss2')
    
    script>
    

    浏览器呈现:

    # 爷爷
    ——————————————————
    # 父亲
    
    来自孩子A: kiss2
    ——————————————————
    # 孩子A
    
    来自父亲:新衣服2
    

    Tip:我们推荐你始终使用 kebab-case 的事件名 —— vue2 官网 - 事件名

    mitt

    在 vue2 中我们学过中央事件总线

    Vue 3中,中央事件总线(Vue 2中的emit/on机制)已被废除。Vue 3更加推崇使用组合 API、provide/inject以及props/emits来进行组件之间的通信。这样的做法使得组件通信更加明确和可追踪,并且更容易维护和理解。而像mitt这样的第三方库可以作为替代方案,用于实现更灵活的事件管理。

    mitt 可以实现任意组件之间的通信。

    pubsub(例如 pubsub-js 库)、$bus(例如 vue2 中的中央事件总线)、mitt 都是前端中常见的用于实现事件总线(Event Bus)或事件订阅-发布(Publish-Subscribe)模式的解决方案。这三者都是一个套路。也就是:

    • 接收数据:提前绑定(订阅数据)
    • 提供数据:适时触发(发布消息)

    mitt 用法很简单,直接看 mitt 仓库。首先下载包:

    PS hello_vue3>  npm install --save mitt
    
    added 1 package, and audited 72 packages in 2s
    
    10 packages are looking for funding
      run `npm fund` for details
    
    1 moderate severity vulnerability
    
    To address all issues, run:
      npm audit fix
    
    Run `npm audit` for details.
    
    "mitt": "^3.0.1"
    

    创建 emitt 并在 main.ts 将其引入项目:

    // src\utils\emitter.ts
    import mitt from 'mitt'
    
    const emitter = mitt()
    
    export default emitter
    
    // 引入
    import emitter from './utils/emitter'
    

    需求:现在我们让 ChildA 给 ChildB 送礼物。

    请看实现:

    ChildA 中触发事件:emitter.emit

    
    <template>
        <p># 孩子Ap>
        <button @click="emitter.emit('send-toy', '篮球')">给兄弟礼物button>
    template>
    
    <script lang="ts" setup name="App">
    import emitter from '@/utils/emitter';
    script>
    

    ChildB 中绑定事件:emitter.on

    
    <template>
        <p># 孩子Bp>
        <p>兄弟送的礼物:{{ gift }}p>
    template>
    
    
    

    在 ChildA 中点击按钮,B就能收到礼物。完成任意组件的通信。

    Tip: 建议组件卸载时解绑事件。就像这样:

    import {onUnmounted} from 'vue'
    
    onUnmounted(() => {
        // 移除该类型的所有事件处理程序
        emitter.off('send-toy')
    })
    

    其他写法有:

    // 监听
    // foo { a: 'b' }
    emitter.on('foo', e => console.log('foo', e) )
    // 触发
    emitter.emit('foo', { a: 'b' })
    
    // 监听所有事件。比如 foo2 就会触发
    // foo2 {a: 'b'}
    emitter.on('*', (type, e) => console.log(type, e) )
    emitter.emit('foo2', { a: 'b' })
    
    // 清除所有事件
    emitter.all.clear()
    
    // 注册和解绑事件
    function onFoo() {}
    emitter.on('foo', onFoo)   // listen
    emitter.off('foo', onFoo)  // unlisten
    

    v-model

    vue2 中 v-model 用于简化父子之间的通信

    你可能不会经常直接在自定义组件中编写 v-model,但是许多 UI 组件库的底层确实会使用 v-model 来简化父子组件之间的通信和数据流动。这种设计可以使得使用这些组件时更加方便和直观。

    举例来说,当你使用一个 UI 组件库提供的输入框组件时,通常可以通过 v-model 来实现父组件与该输入框组件之间的双向绑定,让你可以直接在父组件中操作输入框的值,而不需要手动监听事件或者通过 props 和 emit 进行通信。这种方式大大简化了组件的使用方式和数据流动。

    v-model 作用在 input 上可以实现双向绑定,作用在组件上,也能实现父子组件之间的通信(vue2 v-model数字输入框组件

    v-model 实际上是语法糖,对于 input,等于绑定了 :value 和 @input。就像这样:

    // vue2
    "message" placeholder="edit me"> 
    等于
    type="text" :value="message" @input="message = $event.target.value" placeholder="edit me">
    

    vue3 中 v-model 类似,v-model 对应的是 modelValue 的 prop 和 update:modelValue 的事件。比如我想封装一个 MyInput 组件。

    <MyInput v-model="username"/>
    
    等价
    
    <MyInput 
        :modelValue="username"
        @update:modelValue="username = $event"
    />
    

    需求:组件A使用 MyInput,通过 v-model 实现父子之间的通信。

    首先不用语法糖,实现如下:

    
    <template>
        <p># 组件Ap>
        <p>val: {{ val }}p>
        // 方式1
        <MyInput :modelValue="val" @update:modelValue="changeVal"/>
    template>
    
    <script lang="ts" setup name="App">
    import MyInput from '@/views/MyInput.vue'
    import {ref} from 'vue'
    let val = ref('p')
    
    function changeVal($event: string){
        val.value = $event
    }
    script>
    

    Tipupdate:modelValue 就是事件名,只是包含一个冒号。

    
    
    <script lang="ts" setup name="App">
    import { ref,toRefs } from 'vue'
    const props = defineProps(['modelValue'])
    const emits = defineEmits(['update:modelValue'])
    
    console.log('props: ', props);
    // 组件接手初始值
    // 注:之后父组件的 props 修改后,val 不会在响应,需要自己手动修改 val
    let val = ref(props.modelValue)
    
    function handleInput($event: Event){
        // 断言是一个 input 对象。否则ts报错:没有 value
        val.value = (<HTMLInputElement>$event.target).value
        emits('update:modelValue', val.value)
    }
    script>
    

    浏览器呈现:

    # 组件A
    
    val: p
    
    i am MyInput:
    
    // 这是 input 元素
    p
    

    编辑 input 内容时, val 对应的值也会同步,于是实现了父子之间的通信。

    这三种方式在这里完全可以替换,于是我们知道 v-model 确实就是个语法糖。

    // 方式1
    <MyInput :modelValue="val" @update:modelValue="changeVal"/>
    // 方式2:模板自动对 ref 进行解包
    
    // 方式3
    
    

    重命名 modelValue

    目前属性名和方法名中默认是 modelValue,就像:,希望重命名。

    下面这个例子通过 v-model 同时传2个值,并修改默认值。请看示例:

    
    <template>
        <p># 组件Ap>
        <p>val: {{ val }}p>
        <p>val2: {{ val2 }}p>
        <MyInput v-model:name="val" v-model:age="val2"/>
    template>
    
    <script lang="ts" setup name="App">
    import MyInput from '@/views/MyInput.vue'
    import {ref} from 'vue'
    let val = ref('p')
    let val2 = ref(18)
    script>