单页应用中,页面是由不同的组件构成的,Vue 中也是一样,所以我们对下面的这张图非常熟悉。
这样的拆解很大程度上能够复用代码,降低耦合,但同时也会带来一些副作用。
通常情况下,父子组件之间的数据是通过 props
由父向子传递的,当子组件想要修改数据时,则需要通过 $emit
以事件形式交由父组件完成,而这种交互方式只存在于父子组件之间,多层嵌套的时候,处于内层的组件想要获取外层的数据时,需要外层组件一层一层地将数据向下传递;同理,当内层组件想要修改数据时,也需要将事件一层一层向上传递。
当外层组件向最终接收组件传递数据时,中间经过的每个组件都需要定义 props
去接收并向下传递,这种做法肯定是不太合理的,不仅代码冗余了,而且对于中间不需要数据的组件来说,定义自身不需要的 props
也是一种污染;同理,将事件一层一层向上传递也是不太合理的。
我们都知道,任何单页应用中的组件间都不可能只有简单的父子关系,如果有,说明这个应用并不需要做成单页应用。
那么如何才能减少(或避免)这种情况的发生呢?Vue 中提供了 $attrs
和 $listeners
。
这片文章就来聊一聊 $attrs
和 $listeners
的功能。
释义
首先看一下文档中对这两个属性的释义。
$attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
$listeners
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
这里需要注意的是不含 .native 修饰器的事件监听器,即原生的事件是不包含在内的。
示例
为了更好的演示 $attrs
和 $listeners
的功能,我们将示例中的嵌套层数做的深一些。
曾孙组件
创建一个曾孙组件 great-grandson
:
<template>
<div @click="handleClick">$attrs in great grandson {{$attrs}}</div>
</template>
<script>
export default {
name: "grandson",
methods: {
handleClick() {
this.$emit("customClick", "clicked great grandson");
}
}
};
</script>
<style>
</style>
添加自定义点击事件并在点击时触发这个事件,取名 customClick
,参数为一句话 clicked great grandson
。
在页面上绑定 $attrs
,用于显示效果。
孙组件
创建一个孙组件 grandson
:
<template>
<div>I am grandson
<p @click="handleClick">$attrs in grandson {{$attrs}}</p>
<greatGrandson v-bind="$attrs" v-on="$listeners"></greatGrandson>
</div>
</template>
<script>
import greatGrandson from "./great-grandson";
export default {
name: "grandson",
methods: {
handleClick() {
this.$emit("customClick", "clicked grandson");
}
},
components: {
greatGrandson
}
};
</script>
<style>
</style>
首先还是为组件添加自定义点击事件,取名 customClick
,参数为一句话 clicked grandson
,用以区分,并在点击时触发这个事件。
接着引入曾孙组件 great-grandson
,使用 v-bind="$attrs"
和 v-on="$listeners"
向下传递数据和事件监听器。
子组件
创建一个子组件 son
:
<template>
<div>I am son
<p>$attrs in son {{$attrs}}</p>
<grandson v-bind="$attrs" v-on="$listeners"></grandson>
</div>
</template>
<script>
import grandson from "./grandson";
export default {
name: "son",
data() {
return {};
},
methods: {},
components: {
grandson
}
};
</script>
<style scoped>
</style>
引入孙组件 grandson
,使用 v-bind="$attrs"
和 v-on="$listeners"
向下传递数据和事件监听器。
父组件
创建一个父组件 parent
:
<template>
<div>
<son :sentence="sentence" @customClick="handleClick"></son>
</div>
</template>
<script>
import son from "./son";
export default {
data() {
return {
sentence: "this is a sentence"
};
},
methods: {
handleClick(e) {
console.log(e);
}
},
components: {
son
}
};
</script>
<style scoped>
</style>
在 data
中定义一个变量 sentence
,值为 this is a sentence
。
引入子组件 son
,将 sentence
传入,同时绑定自定义事件 customClick
,在回调函数中将自定义事件的参数打印出来。
效果
在 App.vue
中使用这个 parent
组件:
<template>
<div id="app">
<parent></parent>
</div>
</template>
<script>
import parent from "./components/parent";
export default {
name: "App",
components: {
parent
}
};
</script>
<style>
</style>
运行示例,打开浏览器,页面上的内容将会是下面的样子:
在组件 son
、grandson
和 great-grandson
中都显示了 this is a sentence
,而子组件 son
和孙组件 grandson
中都没有定义 props
,说明 parent
组件中的数据正确的传递到了内部组件中。
打开控制台,分别点击组件 grandson
和 great-grandson
中 $attr
所在的句子,可以看到事件也是生效的:
这就是 $attrs
和 $listeners
的功能,去掉数据和事件在多层嵌套组件中传递时的定义部分。注意,仅仅是定义部分,绑定的步骤还是少不了的,即经过的每一层组件都需要使用 v-bind="$attrs"
和 v-on="$listeners"
。
另外,这两个属性都是只读的,不要试图通过 $attrs
去直接修改原数据。
结语
$attrs
和 $listeners
在创建高级组件的时候还是非常有用的,尤其是在开发组件库的时候。我们无法确定组件的嵌套深度(或者说我们不太会去限制嵌套深度),如果想要使用外层数据并在某些时候修改外层数据时,可以尝试使用这两个属性。