Vue $attrs/$listeners 实践

3,640 阅读2分钟

前言

Vue 2.4.0 增加了 $attrs 和 $listeners 属性,先看一下该API的详细定义:

$attrs:  
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。  
当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),  
并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
$listeners:  
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。  
它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

与创建更高层次的组件相关:provide/inject

Vue 2.2.0 新增了 provide 和 inject 属性,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在上下游关系成立的时间里始终生效。主要为高阶插件/组件库提供用例。

实践

在父子组件中传参我们一般会通过props和emit,如果在高层级组件中,比如A->B->C,A与B互为父子,B与C互为父子,如果数据和方法都储存在A组件,那么C组件想要获得A组件的数据和方法的途径就是:A传给B,B再传给C。
今天就介绍多一种方法:$attrs 和 $listeners,A把B跟C需要的数据和方法都使用特性绑定,B拿到它需要的数据,然后将不需要的通过 v-bind="$attrs" 和 v-on="$listeners" 传递给C组件。

  1. 组件C
//指令
let BaseFocus = {
    inserted(el, binding) {
      el.focus = binding.value
    }
};
let BaseInput = {
    props: {
      type: {
        type: String,
        default: 'text'
      },
      name: {
        type: String,
        default: 'text'
      },
      focus: {
        type: Boolean,
        default: false
      },
      styles: {
        type: Object,
        default: function() {
          return {}
        }
      }
    },
    directives: {
      focus: BaseFocus
    },
    template: `<input 
        :type="type" 
        :name="name" 
        @input="input" 
        @change="change" 
        v-focus="focus" 
        :style="styles" />`,
    methods: {
      input(e) {
        this.$emit("input", e.target.value)
      },
      change(e) {
        this.$emit("change", e.target.value)
      }
    }
};
  1. 组件B
let BaseCard = {
    props: {
      user_info: {
        type: Object,
        default: function() {
          return {}
        }
      }
    },
    components: {
      "base-input": BaseInput
    },
    template: 
      `<div class="base-card">
        <div class="name">姓名:{{user_info.user_name}}</div>
        <div class="gender">性别:{{user_info.user_gender}}</div>
        <div class="test" @click="test">测试:Click me!!</div>
        <div class="remark">
          密码:<base-input v-bind="$attrs" v-on="$listeners"></base-input>
        </div>
      </div>`,
    methods: {
      test() {
        this.$emit("test")
      }
    }
}
  1. 组件A
<div class="container" id="container">
  <base-card 
  :user_info="user_info" 
  v-bind="user_input" 
  @test="test" 
  @input="input"
  @change="change"></base-card>
</div>
  1. Vue实例
let vm = new Vue({
    el: "#container",
    data() {
      return {
        user_info: {
          user_name: "carol",
          user_gender: "female"
        },
        user_input: {
          type: 'password',
          name: 'passowrd',
          focus: true
        }
      }
    },
    components: {
      "base-card": BaseCard
    },
    methods: {
      test() {
        console.log("you just click the test div!!!")
      },
      input(val) {
        console.log("your input: ", val)
      },
      change(val) {
        console.log("you change the input and blur: ", val)
      }
    }
})

在上面的代码中,组件B中包含组件C时,我们的代码是这样写的:

<div class="remark">
  密码:<base-input v-bind="$attrs" v-on="$listeners"></base-input>
</div>

借助这两个新增的API,我们在创建高层级,多层级嵌套的组件时会节约很多时间,代码看起来也会更整洁。
实践最终结果如图所示:

result