没有人教你的——Vue高阶组件用法

825 阅读1分钟

一、$attrs和$listeners如何创建高阶组件

在开始封装高阶组件前前,首先了解一下vue2.4.0 新增的两个创建高阶组件API:

$attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。

1.创建一个子组件(MyButton.vue)

<template>
  <button v-bind="$attrs" v-on="$listeners">
    <slot></slot>
  </button>
</template>

<script>
  export default {
    props: {
      test: String
    },
    mounted() {
      console.log(this.$attrs)  //输出:{"type": "primary"}
    }
  }
</script>

2.创建父组件(Parent.vue),并在父组件中使用MyButton

<template>
  <!-- test属性在MyButton组件中的props中定义了,所以不会被$attrs接收到 -->
  <MyButton test="测试$attrs是否能接收到该属性" type="primary" @click="onclick">
    按钮
  </MyButton>
</template>

以上,在子组件通过给button按钮上绑定$attrs属性可以把props中没有定义的所有属性都绑定到button按钮上,这在使用组件库做二次封装时非常有用,比如我想给button按钮设置type=”primary”,可以直接在父组件中直接传递给MyButton组件,而不用在子组件props中定义接收type属性。简而言之,封装成高阶组件后,自定义的MyButton组件可以像button组件一样使用传递相关属性。

同理,通过绑定$listeners事件监听器,也能够像button组件一样使用传递相关事件监听。

二、$attrs和$listeners在封装input组件时存在的问题

小子我就开门见山了,在封装input组件的高阶组件使用v-model=”value”绑定值时,在input输入框value值改变后,value的值将被赋值为input事件接收的事件对象。下面通过例子详细说明。

1.创建一个子组件(MyInput.vue):

<template>
  <input v-bind="$attrs" v-on="$listeners">
</template>

<script>
  export default {
    watch: {
      '$attrs.value'(newValue) {
        console.log('newValue:', newValue)
      }
    }
  }
</script>

下图是在input输入框手动修改值时,watch监听$attrs.value的输出结果

image.png

image.png

2.在父组件加入MyInput组件

<template>
  <view>
    <!-- test属性在MyButton组件中的props中定义了,所以不会被$attrs接收到 -->
    <MyButton test="测试$attrs是否能接收到该属性" type="primary" @click="onclick">
      按钮
    </MyButton>
    <MyInput v-model="value" @input="oninput"></MyInput> 
  </view>
</template>

<script>
  export default {
    data() {
      return {
        value: '1'
      }
    },
    methods: {
      onclick(e) {
        console.log('onclick', e)
      },
      oninput(e) {
        console.log('oninput', e)
        /**
         * 由于v-model="value"相等于给MyInput组件中的input绑定了:value="value"属性,
         * 所以需要在父组件的input事件手动给value赋值实现双向绑定,
         * 如果没有赋值,则value的值将是参数e事件对象
         */
        this.value = e.detail.value
      }
    }
  }
</script>

三、高阶组件进阶用法——插槽$slots

老套路,先了解一下$slots是什么。

$slots:用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

对$slots有了大致了解后,下面以左边显示属性label名称,右边显示属性值的的list组件为例,展开说明插槽$lost的高阶组件用法。

1.创建List.vue组件

<template>
  <view class="list">
    <slot></slot>
  </view>
</template>

<script>
  export default {
    created() {
      console.log(this.$slots)
      this.$slots.default.forEach(item => {
        item.data.attrs = this.$attrs
      })
    }
  }
</script>

$slots可访问插槽虚拟节点(VNode),这里,没有给slot设置name属性,所以使用默认值default访问所有子节点,如果你给插槽设置了name属性,比如<slot name="body"></slot>,就得使用this.$slots.body访问所有ListItem虚拟节点。

主要实现思路是通过给虚拟节点的attrs属性赋值为$attrs,即可把所有List组件没有定义的props属性全部传递给ListItem的$attrs属性,从而达到通过List组件控制所有ListItem组件的效果。

this.$slots打印内容:

image.png

2.创建ListItem.vue组件

<template>
  <view class="list-item">
    <view class="label" :style="{ width: labelWidth }">{{ label }}:</view>
    <view class="value">{{ value }}</view>
  </view>
</template>

<script>
  export default {
    props: {
      label: String,
      value: [String, Number],
      labelWidth: {
        type: String,
        default() {
          return this.$attrs.labelWidth || 'auto'
        }
      }
    },
    mounted() {
      console.log(this.$attrs)
    }
  }
</script>

<style lang="scss" scoped>
  .list-item {
    display: flex;
    padding: 10px;
  }
</style>

ListItem主要实现是通过给props的labelWidth属性的默认值加上优先使用$attrs.labelWidth属性。如果ListItem传递了labelWidth属性,则使用自身的值。

优先级:

1.通过ListItem传递props的属性值labelWidth;

2.通过List传递的$attrs.labelWidth;

3.ListItem组件prop定义的labelWidth默认值'auto';

最后,进阶版高阶组件实现效果截图:

image.png

姓名使用的是ListItem自身的props属性labelWidth,年龄性别使用的是List组件的labelWidth。

最后,一个简单的高阶组件封装就完成啦!

相关知识点:$attrs,$listeners,$slots。

更多用法欢迎评论区讨论及指正。