39

Vue组件数据通讯新姿势:$attrs 和 $listeners

 5 years ago
source link: https://www.w3cplus.com/vue/vue-js-attrs-and-listeners.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

学习Vue也有一段时间了,在项目中使用Vue也有好几个了,但Vue组件间的状态管理(数据通信)一直是自己的死穴。对于Vue组件间的数据通信,无外呼是父组件向子组件、子组件向父组件、兄弟组件以及嵌套组件之间的数据通信。而且组件之间的通信方式也有很多种。 @Gongph 的《 Vue 父子组件通信的十种方式 》一文就详细的介绍了Vue组件,指的是父子组件之间的数据通信就有差不多十种方式。但很多时候我们组件之间的数据通信不仅仅是停留在父子组件之间的数据通信。比如说还有兄弟组件和嵌套组件之间的数据通信。

如果我们抛开嵌套组件之间的数据通信,我们可以用简单的下图来描述Vue组件之间的数据通信:

biYb6rv.png!web

事实上除了上图方式对数据进行通信之外,还有一些其他的方式,比如父组件获取子组件数据和事件可以通过:

ref
this.$children

对于子组件获取父组件数据和事件,可以通过:

  • 通过 props 传递父组件数据和事件,或者通过 $emit$on 实现事件传递
  • 通过 ref 属性,调用子组件方法,传递数据;通过 props 传递父组件数据和事件,或者通过 $emit$on 实现事件传递
  • 通过 this.$parent.$data 或者 this.$parevent._data 获取父组件数据,通过 this.$parent 执行父组件方法

对于兄弟组件之间数据通信和事件传递,可以通过:

  • 利用 eventBus 挂载全局事件
  • 利用 $parent 进行数据传递, $parent.$children 调用兄弟组件事件

另外,复杂一点的,可以通过Vuex完成Vue组件数据通信。特别是多级嵌套组件间的数据通信。但如果仅仅是数据之间传递,而不做中间处理,使用Vuex有点浪费。不过,自Vue 2.4版本开始提供了另一种方法:

使用 v-bind="$attrs" 将父组件中不被认为 props 特性绑定的属性传递给子组件。

通常该方法会配合 interiAttrs 一起使用。之所以这样使用是因为两者的出现使得组件之间跨组件的通信在不依赖Vuex和 eventBus 的情况下变得简洁,业务清晰。

其实这也就是我们今天要了解的另一个知识点。多级嵌套组件之间,我们如何借助 $attrs$listeners 来实现数据之间的通信。

业务场景

刚才提到过,我们接下来要聊的是多级嵌套组件之间的数据通信。为了让事情不变得太过于复杂(因为太复杂,对于初学者而言不易于理解和学习)。这里我们就拿三级组件之间的嵌套来举例。比如我们有三个组件 ComponentAComponentBComponentC ,而且它们之间的关系是 ComponentA > ComponentB > ComponentC> 是包含关系),用下图来描述或许更易于明白他们之间的关系:

qEVzmq7.png!web

就三级嵌套的组件而言,他们的关系相对而言要简单一些:

  • ComponentA 组件是 ComponentB 组件的父组件,他们的关系是 父子关系
  • ComponentB 组件是 ComponentC 组件的父组件,他们的关系也是 父子关系
  • ComponentA 组件是 ComponentC 组件的祖先组件,他们的关系是 祖孙关系

ARzAbee.png!web

对于这三个组件之间的数据通信,按照我们前面所掌握的知识,估计想到的是:

props 向下, $emit 向上。

7VbAby2.png!web

也就是说, ComponentAComponentB 可以通过 props 的方式向子组件传递, ComponentBComponentA 通过在 ComponentB 组件中 $emit 向上发送事件,然后在 ComponentA 组件中 $on 的方式监听发送过来的事件。对于 ComponentBComponentC 两组件之间的通信也可以使用类似的方式。但对于 ComponentA 组件到 ComponentC 组件之间的通信,需要借助 ComponentB 组件做为 中转站 ,当 ComponentA 组件需要把信息传递给 ComponentC 组件时, ComponentB 接受 ComponentA 组件的信息,然后利用属性传递给 ComponentC 组件。

就此而言,这是一种解决方案,但如果我们嵌套的组件层级过多时将会导致代码繁琐,代码维护也较困难。

除了上述方式可以完成组件之间数据通信外,还有其他的方式,比如借助Vuex的全局状态共享;使用 eventBus 创建Vue的实例实现事件的监听和发布,从而实现组件之间的数据通信。但都过于太浪费,所以我们应该寻找其他更为简易的解决方案,其中文章开始提到的 $attrs 以及 $listeners

简单地说, 利用 $attrs 实现祖孙组件间的数据传递, $listeners 实现祖孙组件间的事件监听 。接下来看看怎么使用这两个特性来完成跨级嵌套组件之间的数据通信。

术语解释

在具体掌握 $attrs$listeners 是如何完成组件数据通信之前,先来简单地了解一下他们具体是什么?

Vue的官网对 $attrs$listeners 的描述分别是这样的:

$attrs 的解释

包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 ( classstyle 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 ( classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件 —— 在创建高级别的组件时非常有用。

$listeners 的解释

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件 —— 在创建更高层次的组件时非常有用。

官方解释的已经非常的清楚了。事实上,你可以把 $attrs$listeners 比作两个集合,其中 $attrs 是一个属性集合,而 $listeners 是一个事件集合,两者都是 以对象的形式来保存数据

更简单地说, 利用 $attrs 实现祖孙组件间的数据传递, $listeners 实现祖孙组件间的事件监听 。而且 $attrs 继承所有的父组件属性(除 props 传递的属性、 classstyle ,一般用在子组件的子元素上; $listeners 是一个对象,里面包含了作用在这个组件上的所有监听器,配合 v-on 将所有事件监听器指向这个组件的某个特定的子元素( 相当于子组件继承父组件的事件 )。

为了更易于帮助大家理解这两个属性,我们还是通过一些简单的示例来演示吧。先来看一个简单的示例:

<!-- ChildComponent.vue -->
<template>
    <div class="child-component">
        <h1>我是一个 {{ professional }}</h1>
    </div>
</template>

<script>
    export default {
        name: 'ChildComponent',
        props: {
            professional: {
                type: String,
                default: '码农'
            }
        },
        created () {
            console.log(this.$attrs, this.$listeners)

            // 调用父组件App.vue中的triggerTwo()方法
            this.$listeners.two()
        }
    }
</script>

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <ChildComponent 
        :professional = "professional"
        :name = "name"
        @one.native = "triggerOne"
        @two = "triggerTwo"
        />
    </div>
</template>

<script>
    import ChildComponent from './components/ChildComponent.vue'

    export default {
        name: 'app',
        data() {
            return {
                professional:  '屌丝码农',
                name:'大漠'
            }
        },
        components: {
            ChildComponent
        },
        methods: {
            triggerOne () {
                console.log('one')
            },
            triggerTwo () {
                console.log('two')
            }
        }
    }
</script>

示例代码可以在Github的 Vue Demos 中获取 app-vue-communication 项目的 step1 分支获取。

yYbMRv2.png!web

从上面的代码中我们可以看出来,在父组件 App.vue 中,调用子组件 ChildComponent 时有两个属性和两个方法,共别是 其中有一个属性是 props 声明的( professional ),事件一个是 .native 修饰器(监听组件根元素的原生事件)

这个简单的示例告诉我们可以通过 $attrs$listeners 进行数据传递,在需要的地方进行调用和处理。比如上面子组件 ChildComponent 中通过 this.$listeners.two() 访问了父组件 App.vue 中的 triggerTwo() 方法。当然,我们还可以通过 v-on="$listeners" 一级级地往下传递,不管组件嵌套层级有多深。这个后面我们会详细介绍。

另外,上面的示例中,其中有一个属性是 props ,比如 professional 属性,另外还有一个非 props 属性,比如 name 。组件编译之后会把非 props 属性当成原始属性对待,从而添加到DOM元素(HTML标签上),比如上例中的 name

EbyAVnM.png!web

这样的结果或许并不是大家所想要的,如果想去掉HTML标签中 name 的属性,以至于该属性不暴露出来,我们可以借助 inheritAttrs 属性来完成。

inheritAttrs 的默认值 true ,继承所有的父组件属性(除 props 的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置 inheritAttrs: false ,但是 class 属性会继承。简单的说,** inheritAttrs:true 继承除 props 之外的所有属性; inheritAttrs:false 只继承 class 属性**。

如果我们在子组件 ChildComponent 中添加 inheritAttrs: false ,重新编译出来的代码中 name (非 props )属性再不会暴露出来:

7nUZb2n.png!web

多级嵌套组件数据通信

前面花了很长的篇幅解释了 $attrs$listeners 以及它们是如何在组件中进行数据通信的。回到我们的示例中来,看看文章开头提以的三级嵌套组件之间的数据是如何借助 $attrs$listeners 实现数据通信。具体代码可以将分支切换到 step2 中:

<!-- ComponentC.vue -->
<template>
    <div class="component-c">
        <h3>组件C中设置的props: {{ name }}</h3>
        <p>组件C中的$attrs: {{ $attrs }}</p>
        <p>组件C中的$listeners: {{ $listeners }}</p>
    </div>
</template>

<script>
    export default {
        name: 'ComponentC',
        props: {
            name: {
                type: String,
                default: '大漠'
            }
        },
        inheritAttrs: false,
        mounted () {
            this.$emit('test2')
            console.log('ComponentC',this.$attrs, this.$listeners)
        }
    }
</script>

<!-- ComponentB.vue -->
<template>
    <div class="component-b">
        <h3>组件B中的props: {{ age }}</h3>
        <p>组件B中的$attrs: {{ $attrs }}</p>
        <p>组件B中的$listeners: {{ $listeners }}</p>

        <hr />
        <ComponentC v-bind="$attrs" v-on="$listeners" />
    </div>
</template>

<script>
    import ComponentC from './ComponentC'

    export default {
        name: 'ComponentB',
        props: {
            age: {
                type: Number,
                default: 30
            }
        },
        inheritAttrs: false,
        components: {
            ComponentC
        },
        mounted () {
            this.$emit('test1')
            console.log('ComponentB',this.$attrs, this.$listeners)
        }
    }
</script>

<!-- ComponentA.vue -->
<template>
    <div class="component-a">
        <ComponentB :name="name" :age="age"  @on-test1="onTest1" @on-test2="onTest2" />
    </div>
</template>

<script>
    import ComponentB from './ComponentB'

    export default {
        name: 'ComponentA',
        components: {
            ComponentB
        },
        data () {
            return {
                name: '大漠_w3cplus',
                age: 23
            }
        },
        methods: {
            onTest1 () {
                console.log('test1 runing...')
            },
            onTest2 () {
                console.log('test2 running...')
            }
        }
    }
</script>

<!-- App.vue -->
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <ComponentA />
    </div>
</template>

<script>
    import ComponentA from './components/ComponentA.vue'

    export default {
        name: 'app',
        components: {
            ComponentA
        }
    }
</script>

这个时候你在页面中将看到的结果如下:

rEZJVze.png!web

其于上面的基础上,我们来看一个简单的示例(切到分支 step3 ),一个模态框的数据通信:

<!-- ModalHeader.vue -->
<template>
    <div class="modal-header">
        <h5 class="modal-title">{{ modalTitle }}</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="close">
        <span aria-hidden="true">×</span>
        </button>
    </div>
</template>

<script>
export default {
    name: 'ModalHeader',
    props: {
        modalTitle: {
            type: String,
            default: 'Modal Title'
        }
    },
    inheritAttrs: false,
    methods: {
        close () {
            this.$emit('on-close')
        }
    },
    mounted () {
        console.log('ModalHeader',this.$attrs, this.$listeners)
    }
}
</script>

<!-- ModalBody.vue -->
<template>
    <div class="modal-body">
        <slot>{{ modalContent }}</slot>
    </div>
</template>

<script>
export default {
    name: 'ModalBody',
    props: {
        modalContent: {
            type: String,
            default: 'Modal body text goes here.'
        }
    },
    inheritAttrs: false,
    mounted () {
        console.log('ModalBody',this.$attrs, this.$listeners)
    }
}
</script>

<!-- ModalFooter.vue -->
<template>
    <div class="modal-footer">
        <button class="btn btn-secondary" data-dismiss="modal" @click="close">{{ secondaryButtonContent }}</button>
        <button class="btn btn-primary" @click="save">{{ primaryButtonContent }}</button>
    </div>
</template>

<script>
export default {
    name: 'ModalFooter',
    props: {
        secondaryButtonContent: {
            type: String,
            default: 'Close'
        },
        primaryButtonContent: {
            type: String,
            default: 'Save'
        }
    },
    inheritAttrs: false,
    methods: {
        save () {
            this.$emit('on-save')
        },
        close () {
            this.$emit('on-close')
        }
    },
    mounted () {
        console.log('ModalFooter',this.$attrs, this.$listeners)
    }
}
</script>

<!-- Modal.vue -->
<template>
    <div class="modal" tabindex="-1" role="dialog" v-if="show">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <ModalHeader v-bind="$attrs" v-on="$listeners" />
                <ModalBody v-bind="$attrs" v-on="$listeners" />
                <ModalFooter v-bind="$attrs" v-on="$listeners" />
            </div>
        </div>
    </div>
</template>

<script>
import ModalHeader from './ModalHeader'
import ModalBody from './ModalBody'
import ModalFooter from './ModalFooter'

export default {
    name: 'Modal',
    props: {
        show: {
            type: Boolean,
            default: false
        }
    },
    components: {
        ModalHeader,
        ModalBody,
        ModalFooter
    },
    inheritAttrs: false,
}
</script>

<!--  MaskBackdrop.vue -->
<template>
    <div class="modal-backdrop" v-if="show" @click="close">
    </div>
</template>

<script>

export default {
    name: 'MaskBackdrop',
    props: {
        show: {
            type: Boolean,
            default: false
        }
    },
    inheritAttrs: false,
    mounted () {
        console.log('MaskBackdrop',this.$attrs, this.$listeners)
    },
    methods: {
        close () {
            this.$emit('on-close')
        }
    }
}
</script>

你将看到的效果如下:

RfUjiqf.gif

在浏览器调试器中,我们可以看以相应 $attrs$listeners 打印出来的值:

zIZ32mz.png!web

小结

啰嗦了这么多,主要就是阐述了Vue 2.4版本之后的 $attrs$listeners 是什么以及怎么利用他们来实现组件之间的数据通信。使用这两个特性可以实现跨组件(嵌套)组件之间的数据通信。最后希望这篇文章对大家或多或少有所收获。结合前面的教程,我们可以了解到组件之间数据通信有很多种方式,具体哪种更好应该根据不同的场景来对待,选择最适合的。如果您在这方面有更多的经验或者文章中有不正之处,烦请路过的大神多多拍正。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK