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

栏目: JavaScript · 发布时间: 5年前

内容简介:学习Vue也有一段时间了,在项目中使用Vue也有好几个了,但Vue组件间的状态管理(数据通信)一直是自己的死穴。对于Vue组件间的数据通信,无外呼是父组件向子组件、子组件向父组件、兄弟组件以及嵌套组件之间的数据通信。而且组件之间的通信方式也有很多种。如果我们抛开嵌套组件之间的数据通信,我们可以用简单的下图来描述Vue组件之间的数据通信:

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

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

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

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

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> 是包含关系),用下图来描述或许更易于明白他们之间的关系:

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

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

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

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

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

props 向下, $emit 向上。

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

也就是说, 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 分支获取。

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

从上面的代码中我们可以看出来,在父组件 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

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

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

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

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

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

多级嵌套组件数据通信

前面花了很长的篇幅解释了 $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>

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

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

其于上面的基础上,我们来看一个简单的示例(切到分支 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>

你将看到的效果如下:

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

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

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

小结

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

国际游戏设计全教程

国际游戏设计全教程

[美]迈克尔·萨蒙德 / 张然、赵嫣 / 中国青年出版社 / 2017-2 / 108.00元

你想成为一名电子游戏设计师吗?想知道《肯塔基0号路》《到家》《枪口》等独立游戏的制作理念及过程吗?想了解《戈莫布偶大冒险》《辐射3》《战争机器》中关卡设计的奥秘吗?本书用通俗易懂的文字介绍了在游戏开发与策划过程中,需要掌握的游戏设计原理和制作的基础知识,可以作为读者从“构思一个电子游戏”到“真正完成一个电子游戏”的完备指南。 本书以系统的游戏设计流程结合大量优秀的游戏设计案例进行讲解,让读者......一起来看看 《国际游戏设计全教程》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试