单页应用中,页面是由不同的组件构成的,Vue 中也是一样,所以我们对下面的这张图非常熟悉。

components

这样的拆解很大程度上能够复用代码,降低耦合,但同时也会带来一些副作用。

通常情况下,父子组件之间的数据是通过 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>

运行示例,打开浏览器,页面上的内容将会是下面的样子:

result

在组件 songrandsongreat-grandson 中都显示了 this is a sentence,而子组件 son 和孙组件 grandson 中都没有定义 props,说明 parent 组件中的数据正确的传递到了内部组件中。

打开控制台,分别点击组件 grandsongreat-grandson$attr 所在的句子,可以看到事件也是生效的:

event

这就是 $attrs$listeners 的功能,去掉数据和事件在多层嵌套组件中传递时的定义部分。注意,仅仅是定义部分,绑定的步骤还是少不了的,即经过的每一层组件都需要使用 v-bind="$attrs"v-on="$listeners"

另外,这两个属性都是只读的,不要试图通过 $attrs 去直接修改原数据。

结语

$attrs$listeners 在创建高级组件的时候还是非常有用的,尤其是在开发组件库的时候。我们无法确定组件的嵌套深度(或者说我们不太会去限制嵌套深度),如果想要使用外层数据并在某些时候修改外层数据时,可以尝试使用这两个属性。

最近更新:
作者: MeFelixWang