一、$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的输出结果
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打印内容:
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';
最后,进阶版高阶组件实现效果截图:
姓名使用的是ListItem自身的props属性labelWidth,年龄和性别使用的是List组件的labelWidth。
最后,一个简单的高阶组件封装就完成啦!
相关知识点:$attrs,$listeners,$slots。
更多用法欢迎评论区讨论及指正。