Vue组件通信之$attrs、$listeners


前言

vue通信手段有很多种,props/emitvuexevent busprovide/inject等。还有一种通信方式,那就是 $attrs$listeners,之前早就听说这两个api,趁着有空来补补。这种方式挺优雅,使用起来也不赖。下面例子都会通过父、子、孙子,三者的关系来说明使用方式。

Vue 3 注意事项:
在 Vue 3 中,$listeners 已被移除,其功能被合并到 $attrs 中。现在 $attrs 不仅包含属性,还包含事件监听器。

img

$attrs

官方解释

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

我的理解

接收除了props声明外的所有绑定属性(class、style除外)

图解:

img

由于child.vue 在 props 中声明了 name 属性,$attrs 中只有agegender两个属性,输出结果为:

{ age: "20", gender: "man" }

img
上面打印结果为:

{height: '80', name: 'joe', gender: 'man'}//会把child页面的值,一起传过来

注:但若是child.vue页面有个prop:[“name”],则name不会传到grandson.vue页面中去,即:{height: '80', gender: 'man'},如下:

8

另外可以在 grandson.vue 上通过 v-bind="$attrs",可以将属性继续向下传递,让 grandson.vue 也能访问到父组件的属性,这在传递多个属性时会显得很便捷,而不用一条条的进行绑定。

如果想要添加其他属性,可继续绑定属性。但要注意的是,继续绑定的属性和 $attrs 中的属性有重复时,继续绑定的属性优先级会更高。

9
打印结果为:

{height: '80', name: 'xth', gender: 'man'}//会把child页面的值,一起传过来,同时会以继续绑定的属性为最高优先级

$listeners

官方解释:

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

我的理解:

接收除了带有.native事件修饰符的所有事件监听器

图解:

img

parent.vue 中对 child.vue 绑定了两个事件,带有.nativeclick 事件和一个自定义事件,所以在 child.vue 中,输出 $listeners 的结果为:

{
    customEvent: fn;
}

img

attrs 属性一样,可以通过 v-on="$listeners",将事件监听器继续向下传递,让 grandson.vue 访问到事件,且可以使用 $emit 触发 parent.vue 的函数。

如果想要添加其他事件监听器,可继续绑定事件。但要注意的是,继续绑定的事件和 $listeners 中的事件有重复时,不会被覆盖。当 grandson.vue 触发 customEvent 时,child.vue 和 parent.vue 的事件都会被触发,触发顺序类似于冒泡,先到 child.vue 再到 parent.vue。

以上转自:Vue - 组件通信之$attrs、$listeners

代码示例

1.$attrs 示例

<div id="app">
    根组件
    <hr />
    <father :msg="msg" :rootName="name" :rootDetail="infor"></father>
</div>
<template id="father">
    <div>
        父组件---{{msg}}
        <hr />
        <son v-on="$listeners" v-bind="$attrs"></son>
    </div>
</template>

<template id="son">
    <div>子组件</div>
</template>
<script>
    // 子组件
    let son = {
        template: "#son",
        created() {
            console.log(this.$attrs, "子组件,接受root的非prop的数据");
        },
    };

    // 父组件
    let father = {
        template: "#father",
        components: {
            son,
        },
        props: ["msg"],
        created() {
            console.log(this.$attrs, "父组件,接受root的非prop的数据");
        },
    };

    let vm = new Vue({
        el: "#app",
        components: {
            father,
        },
        data() {
            return {
                msg: "根结点的数据",
                name: "root",
                infor: {
                    name: "xth",
                    age: 18,
                    address: "app根部",
                },
            };
        },
    });
</script>

img

2.$listeners 示例

<div id="app">
    根组件
    <hr />
    <father @cli1="click1" @cli2="click2" @cli3.native="click3"></father>
</div>
<template id="father">
    <div v-on="$listeners">
        父组件 <button @click="$listeners.cli1">获取root方法cli1</button><br />
        <hr />
        <son v-on="$listeners"></son>
    </div>
</template>

<template id="son">
    <div>
        子组件
        <button @click="$listeners.cli2">获取root方法cli2</button>
    </div>
</template>
<script>
    // 子组件
    let son = {
        template: "#son",
        created() {
            console.log(this.$listeners, "子组件"); // 包含父级所有绑定的方法
        },
    };

    // 父组件
    let father = {
        template: "#father",
        components: {
            son,
        },
        created() {
            console.log(this.$listeners, "父组件,接受父级所有绑定的方法"); // 包含父级所有绑定的方法
        },
    };

    let vm = new Vue({
        el: "#app",
        components: {
            father,
        },
        methods: {
            click1() {
                console.log("点击事件cli1");
            },
            click2() {
                console.log("点击事件cli2");
            },
        },
    });
</script>

img

3.$attrs 、$listeners 、 prop 、 $emit 示例

<div id="app">
    根组件
    <hr />
    <father
        :msg="msg"
        :rootName="name"
        :rootDetail="infor"
        @cli1="click1"
        @cli2="click2"
        @cli3.native="click3"
    ></father>
</div>
<template id="father">
    <div v-on="$listeners">
        父组件---{{msg}} <button @click="$listeners.cli1">获取root方法cli1</button><br />
        <button @click="getRootMethod">获取root方法</button>
        <hr />
        <son v-on="$listeners" v-bind="$attrs" @fathermethod="fatherMethod"></son>
    </div>
</template>

<template id="son">
    <div>
        子组件
        <button @click="$listeners.cli2">获取root方法cli2</button>
        <button @click="getFatherMethod">获取father方法</button>
    </div>
</template>
<script>
    // 子组件
    let son = {
        template: "#son",
        created() {
            console.log(this.$attrs, "子组件,接受root的非prop的数据");
            console.log(this.$listeners, "子组件"); // 包含父级所有绑定的方法
        },
        methods: {
            getFatherMethod() {
                this.$emit("fathermethod");
            },
        },
    };

    // 父组件
    let father = {
        template: "#father",
        components: {
            son,
        },
        props: ["msg"],
        created() {
            console.log(this.$attrs, "父组件,接受root的非prop的数据");
            console.log(this.$listeners, "父组件,接受父级所有绑定的方法"); // 包含父级所有绑定的方法
        },
        methods: {
            getRootMethod() {
                this.$parent.rootMethod();
            },
            fatherMethod() {
                console.log("子组件的getFatherMethod方法点了我");
            },
        },
    };

    let vm = new Vue({
        el: "#app",
        components: {
            father,
        },
        data() {
            return {
                msg: "根结点的数据",
                name: "root",
                infor: {
                    name: "xth",
                    age: 18,
                    address: "app根部",
                },
            };
        },
        methods: {
            click1() {
                console.log("点击事件cli1");
            },
            click2() {
                console.log("点击事件cli2");
            },
            click3() {
                console.log("点击事件cli3");
            },
            rootMethod() {
                console.log("父组件的getRootMethod方法点了我");
            },
        },
    });
</script>

Vue 3 中的使用方式

在 Vue 3 中,$listeners 已被移除,所有事件监听器现在都包含在 $attrs 中。以下是 Vue 3 的示例:

Vue 3 示例

<!-- App.vue (根组件) -->
<template>
    <div>
        根组件
        <hr />
        <Father :msg="msg" :rootName="name" :rootDetail="infor" @cli1="click1" @cli2="click2" />
    </div>
</template>

<script setup>
    import { ref, reactive } from "vue";
    import Father from "./Father.vue";

    const msg = ref("根结点的数据");
    const name = ref("root");
    const infor = reactive({
        name: "xth",
        age: 18,
        address: "app根部",
    });

    const click1 = () => {
        console.log("点击事件cli1");
    };

    const click2 = () => {
        console.log("点击事件cli2");
    };
</script>

<!-- Father.vue (父组件) -->
<template>
    <div>
        父组件---{{ msg }} <button @click="$attrs.onCli1">获取root方法cli1</button><br />
        <hr />
        <Son v-bind="$attrs" @fathermethod="fatherMethod" />
    </div>
</template>

<script setup>
    import { defineProps } from "vue";
    import Son from "./Son.vue";

    const props = defineProps(["msg"]);

    const fatherMethod = () => {
        console.log("子组件的getFatherMethod方法点了我");
    };
</script>

<!-- Son.vue (子组件) -->
<template>
    <div>
        子组件
        <button @click="$attrs.onCli2">获取root方法cli2</button>
        <button @click="getFatherMethod">获取father方法</button>
    </div>
</template>

<script setup>
    import { defineEmits } from "vue";

    const emit = defineEmits(["fathermethod"]);

    const getFatherMethod = () => {
        emit("fathermethod");
    };
</script>

Vue 3 中的变化

  1. $listeners 被移除:所有事件监听器现在都包含在 $attrs
  2. 事件访问方式:在 Vue 3 中,事件监听器以 on 开头的属性形式存在于 $attrs 中(例如 $attrs.onCli1
  3. 组合式 API:使用 definePropsdefineEmits 来定义组件的 props 和事件
  4. v-bind="$attrs":现在会同时传递属性和事件监听器

这种变化使得组件通信更加简洁,不再需要分别处理 $attrs$listeners


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录