$attrs与 $listeners

多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法---- $attrs/ $listeners

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件;它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。
  • inheritAttrs:默认值true,继承所有的父组件属性(除props的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置inheritAttrs: false,但是class属性会继承

主要用途

用在父组件传递数据给子组件或者孙组件

父组件

<template>  
 <div class="hello"\>  
 <h2>父组件浪里行舟</h2>  
 <child  
  :foo = "foo"  
  :boo = "boo"  
  :coo = "coo"  
  :doo = "doo"  
  title = "前端公虾米"  
 @func1='Func1Click'
 @func2='Func2Click'  
  \></child>  
 </div></template>  
  
<script>  
import child from './child'  
export default {  
  name: 'HelloWorld',  
  data () {  
    return {  
      foo: "Javascript",  
  boo: "Html",  
  coo: "CSS",  
  doo: "Vue"  
  }  
  },  
 methods:{
  Func1Click(val) {
     this.second = val 
     }, 
  Func2Click(val) { 
     this.third = val
     } 
 },  
  components:{  
    child  
  }  
}  
</script>

子组件

<template>  
 <div> <h2>子组件</h2>  
 <p>foo:foo作为props属性绑定 {{ foo }}</p>  
 <p>child的$attrs: {{ $attrs }}</p>  
 <son v-bind="$attrs" v-on="$listeners"\></son>  
 </div></template>  
<script>  
  import Son from './son'  
  export default {  
    components: {Son},  
  // inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性  
  props: {  
      foo: String // foo作为props属性绑定  
  },  
  created() {  
      console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }  
  console.log(this.$listeners);  
  }  
  
  }  
</script>

孙组件:

<template>  
 <div class="border"\>  
 <h2>孙组件</h2>  
 <p>boo: {{ boo }}</p>  
 <p>childCom2: {{ $attrs }}</p>  
 <button @click='handleFunc1'>fuc1 button</button>
 <button @click='handleFunc2'>fuc2 button</button>
 <child-com3 v-bind="$attrs"\></child-com3>  
 </div></template>  
<script>  
  import childCom3 from './child3'  
  export default {  
    components: {  
      childCom3  
    },  
  inheritAttrs: false,  
  props: {  
      boo: String  
    },  
  created() {  
      console.log(this.$attrs); // {  "coo": "CSS", "doo": "Vue", "title": "前端工匠" }  
  } ,  
methods: {  
  handleFunc1() {  
    this.$listeners.func1('props changed second')  
  },  
  handleFunc2() {  
    this.$listeners.func2('props changed third')  
  }  
}
  }  
</script>

重孙组件:

<template>  
 <div class="border"\>  
 <h2>重孙组件</h2>  
 <p>childCom3: {{ $attrs }}</p>  
 </div></template>  
<script>  
  export default {  
        name: "child3",  
  props: {  
          coo: String,  
  title: String  
        },  
  created() {  
        console.log(this.$attrs); // {  "coo": "CSS", "doo": "Vue", "title": "前端工匠" }  
  }  
    }  
</script>

inheritAttrs

默认情况下父作用域的不被认作 props 的特性绑定将会 ‘回退’ 且作为普通的 HTML 特性应用在子组件的跟元素上。 当撰写包裹一个目标元素或者另一个组件的组件时,这可能不会总是符合预期行为。 通过设置 inheritAttrs 为 false, 这些默认行为将会被去掉。 而通过实例属性 $attrs 可以让这些特性生效, 而且可以通过 v-bind 显性的绑定到非跟元素上。 (注意:这个选项不影响 class 和 style 绑定)

在子组件中我们没有做【inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性】,现在看子组件在浏览器上的渲染

image.png

当我们添加上这个属性:

image.png

小结:

父组件向子组件传递了boo`coofoodootitle`两个参数, 但是在 子组件内部`props`只接收了一个参数`foo`, 并没有接收`boocoodootitle, 没有接收的多余的属性将会作为子组件根元素的属性节点。html`形式就会显示为:

<div data-v-469af010="" boo="Html" coo="CSS" doo="Vue" title="前端公虾米"></div>

发生上面的情况就是因为 inheritAttrs 选项默认是 true , 但是这和 $attrs 又有什么关系呢?

再来解读一下官网上面的最后几句:

通过设置 inheritAttrs 为 false, 这些默认行为将会被去掉。 而通过实例属性 $attrs 可以让这些特性生效, 而且可以通过 v-bind 显性的绑定到非跟元素上。

也就是说,如果在子组件内将 inheritAttras 选项设置为 false ,我们能够通过 $attrs 这个属性拿到组件 props 中没有定义但是父组件中有传递的属性值:

子组件:

created() {  
  console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }  
}

image.png

$attrs

上面只是讲了 结合inheritAttrs获取$attrs属性, 这两者的结合最厉害的地方在于实现深层次组件之间的传值。

<div>  
 <h2>子组件1</h2>  
 <p>foo:foo作为props属性绑定 {{ foo }}</p>  
 <p>child的$attrs: {{ $attrs }}</p>  
 <son v-bind="$attrs" v-on="$listeners"\></son>  
</div>

image.png

<div class="border"\>  
 <h2>孙组件</h2>  
 <p>boo: {{ boo }}</p>  
 <p>childCom2: {{ $attrs }}</p>  
 <button @click='handleFunc1'>fuc1 button</button> <button @click='handleFunc2'>fuc2 button</button>
 <child-com3 v-bind="$attrs"\></child-com3>  
</div>

image.png

需要注意的两点:

  • 作为中间传递数据的组件,必须要把 inheritAttrs 这个选项设置为 false 才能正确获取 $attrs 数据。
  • v-bind 在平常使用时都是使用 v-bind:src=xx 或者 :src=xx 这种形式, 这里使用 v-bind=xx 是指绑定一个包含键值对的对象到组件。

listeners

上面讲了使用 $attrs 实现数据的向下传递, 但是又**怎么实现下层数据或事件的向上交互呢?** 这里我们要使用到 $listeners

$listeners 和 $attrs 两者表面上都是一个意思,attrs是向下传递数据,listeners 是向下传递方法,通过手动去调用 $listeners 对象里的方法,来触发从父级接受来的函数。

对比父组件,子组件和孙组件:

同 $attrs 的使用相似, 中间组件对于父级传递的事件并没有使用,也是作为一个过度,使用 v-on='this.$listeners' 将从父级接受来的事件再次向下传递,直到传递给最后一级组件, 最后一级的组件就能够使用 this.$listeners 来调用父级的事件,从而改变父级绑定在子组建上面的属性值。

provide/inject

简单来说:就是父组件通过provide来提供数据;子组件通过inject来读取数据

注意: 这里不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据,而不局限于只能从当前父组件的props属性中回去数据

// A.vue
 
<template>
  <div>
    <comB></comB>
  </div>
</template>
 
<script>
  import comB from '../components/test/comB.vue'
  export default {
    name: "A",
    provide: {
      for: "demo"
    },
    components:{
      comB
    }
  }
</script>
// B.vue
 
<template>
  <div>
    {{demo}}
    <comC></comC>
  </div>
</template>
 
<script>
  import comC from '../components/test/comC.vue'
  export default {
    name: "B",
    inject: ['for'],
    data() {
      return {
        demo: this.for
      }
    },
    components: {
      comC
    }
  }
</script>
// C.vue
<template>
  <div>
    {{demo}}
  </div>
</template>
 
<script>
  export default {
    name: "C",
    inject: ['for'],
    data() {
      return {
        demo: this.for
      }
    }
  }
</script>

一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系

注意: 这里不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据,而不局限于只能从当前父组件的props属性中回去数据.

ref / refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果**用在子组件上,引用就指向组件实例**,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref 来访问组件的例子:

// 子组件 A.vue
 
export default {
  data () {
    return {
      name: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}
// 父组件 app.vue
 
<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.name);  // Vue.js
      comA.sayHello();  // hello
    }
  }
</script>

$children / $parent

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <com-a></com-a>
    <button @click="changeA">点击改变子组件值</button>
  </div>
</template>
 
<script>
import ComA from './test/comA.vue'
export default {
  name: 'HelloWorld',
  components: { ComA },
  data() {
    return {
      msg: 'Welcome'
    }
  },
 
  methods: {
    changeA() {
      // 获取到子组件A
      this.$children[0].messageA = 'this is new value'
    }
  }
}
</script>
// 子组件中
<template>
  <div class="com_a">
    <span>{{messageA}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      messageA: 'this is old'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

要注意边界情况,如在#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组。也要注意得到$parent和$children的值不一样,$children 的值是数组,而$parent是个对象

总结:

上面两种方式用于父子组件之间的通信, 而使用props进行父子组件通信更加普遍; 二者皆不能用于非父子组件之间的通信。

总结

常见使用场景可以分为三类:

父子组件通信: props; $parent / $children; provide / inject ; ref ; $attrs / $listeners

兄弟组件通信: eventBus ; vuex

跨级通信: eventBus;Vuex;provide / inject 、$attrs / $listeners


素素
37 声望0 粉丝