刚才在 b 站学了两个二次封装组件的小技巧 (b 站果然是个学习平台)
- 双向数据绑定 2. 向子组件传递插槽 3. 获取子组件的 ref
我们使用 vite 初始化一个 vue、ts 的项目,然后安装下 element-plus
下边开始展示 猛击访问 github 仓库
双向数据绑定
我们以 input 组件作为例子
双向数据绑定的原理及实现想必大家已经烂熟于心了直接看官网吧!
子组件接受一个 modelValue 的 prop, 通过 emit 触发 update:modelValue 事件完成数据的更新
父组件直接 v-model="xxxx"
嫌麻烦官方还提供了 defineModel()
用于简化上边的步骤
向子组件传递插槽
我们以 input 组件作为例子,创建一个 WrapInput.vue
组件
未学习之前
WrapInput.vue
常规的做法,遍历 $slots
来实现
<script setup lang="ts">
const model = defineModel()
</script>
<template>
<el-input v-model="model" placeholder="Please input" >
<template v-for="(_, slot) in $slots" :key="solt" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps"></slot>
</template>
</el-input>
</template>
<style lang='scss' scoped></style>
在 app.vue
中引入并传递 prepend、append 插槽
<script setup lang="ts">
import { ref } from "vue";
import WrapInput from "./components/WrapInput.vue";
const inputText = ref('')
</script>
<template>
<WrapInput v-model="inputText">
<template #prepend>Http://</template>
<template #append>.com</template>
</WrapInput>
<div>
{{inputText}}
</div>
</template>
<style scoped>
</style>
正确渲染了插槽
学习之后
让我们来修改下 WrapInput.vue
<script setup lang="ts">
import { h } from "vue";
import { ElInput } from "element-plus";
const model = defineModel()
</script>
<template>
<component :is="h(ElInput, $attrs, $slots)" v-model="model"></component>
</template>
<style lang='scss' scoped></style>
app.vue
的代码不做任何修改
插槽正常传递、数据更新正常,看到这种写法的时候有点震惊的
component 组件为什么可以传入 h
函数
看下 h 函数的文档, h(ElInput, $attrs, $slots)
是创建了一个虚拟 dom 节点
而 component
组件的 is 属性则可以接收
- 被注册的组件名
- 导入的组件对象
- 一个返回上述值之一的函数
当 component
组件的 is 属性接收到一个函数时,Vue 会调用这个函数并使用其返回值作为要渲染的组件。
在这种情况下,h(ElInput, $attrs, $slots)
会立即执行并返回一个 VNode,这个 VNode 描述了如何渲染 ElInput
组件。
获取子组件的 ref
未学习之前
之前的自己的写法有点蠢的具体的做法是在子组件创建一个 getRef 的函数把 ref 暴露出去,父组件调用 getRef 方法后在执行子组件方法的调用,大概是下边这样
WrapInput1.vue
<script setup lang="ts">
import { h, ref} from "vue";
import { ElInput } from "element-plus";
const model = defineModel()
const inputRef = ref()
function getRef () {
return inputRef.value
}
defineExpose({
getRef
})
</script>
<template>
<component ref="inputRef" :is="h(ElInput, $attrs, $slots)" v-model="model"></component>
</template>
<style lang='scss' scoped></style>
学习之后
WrapInput.vue
<script setup lang="ts">
import { h, ref } from "vue";
import { ElInput } from "element-plus";
const model = defineModel()
const inputRef = ref()
defineExpose(new Proxy({}, {
get(_target, prop) {
return inputRef.value?.[prop]
},
has (_target, prop) {
return prop in inputRef.value
}
}))
</script>
<template>
<component :is="h(ElInput, $attrs, $slots)" v-model="model" ref="inputRef"></component>
</template>
<style lang='scss' scoped></style>
使用 Proxy
代理暴露出去的方法,是有点震惊的,还能这么写
App.vue
<script setup lang="ts">
import { ref } from "vue";
import WrapInput from "./components/WrapInput.vue";
const inputText = ref('')
const prependSlotText = ref('Http://')
const appendSlotText = ref('.com')
function updateSlotInfo (){
prependSlotText.value = 'https://'
appendSlotText.value = `${new Date().getTime()}`
}
const wrapInputRef = ref()
function setWrapInputFocus () {
wrapInputRef.value?.focus()
}
</script>
<template>
<WrapInput v-model="inputText" ref="wrapInputRef">
<template #prepend>{{ prependSlotText }}</template>
<template #append>{{ appendSlotText }}</template>
</WrapInput>
<div style="margin: 20px 0;">
{{inputText}}
</div>
<el-button type="primary" @click="updateSlotInfo">更新插槽内容</el-button>
<el-button type="primary" @click="setWrapInputFocus">set input focus</el-button>
</template>
<style scoped>
</style>
调用组件的 focus
方法让 WrapInput.vue
组件获取焦点
使用 useTemplateRef
会有 ts 类型错误,可尝试按照如下方式修改
const SimpleUserList = defineAsyncComponent(() => import('./SimpleUserList.vue'))
// 获取组件类型
type SimpleUserListInstance = InstanceType<typeof SimpleUserList>
const simpleUserListRef = useTemplateRef('simpleUserListRef')
defineExpose(new Proxy({} as SimpleUserListInstance, {
get(_target, prop) {
return simpleUserListRef.value?.[prop as keyof SimpleUserListInstance]
},
has(_target, prop) {
return prop in (simpleUserListRef.value ?? {})
},
}))
监听子组件的生命周期事件
3.4 版本之前是 @vnode:XXX
总结
本文实践了在 vue3 中在二次封装组件时如何实现 v-model、插槽传递、子组件 ref 获取
插槽传递通过向 component
组件的 is 属性传递 h 函数创建虚拟 dom 来实现
获取子组件的 ref 则是使用 new Proxy
的方式来实现