参考视频:
前言
h 函数在 Vue 中是一个非常重要的概念,它对于我们理解 Vue 里的渲染机制很关键。
当我们在编写 Vue 的 template 模板时,实际上并不是在写 HTML,而是在以一种更直观的方式编写对 h函数的调用。h 函数的作用是创建虚拟节点(VNode)。 ——— 类似于 React 中的 createElement。
在学习 h 函数后,如果你在某些业务场景厌倦使用 Template 的组件编写方式, 完全可以在 Vue 中像 React 那样,以函数式的编程方式来编写组件(不依赖 JSX 插件)。
h 函数参数
- h 函数的第一个参数是 DOM 的节点类型,或者是一个组件;
- 第二个参数是 DOM 的属性 (
attrs),或者组件的props - 第三个参数需要区分
- 如果第一个参数普通 DOM 标签,第三个参数是子节点 (children);
- 如果第一个参数是一个组件,第三个参数就算一个对象,用于传递插槽 (slots)
Template 如何转换成 h 函数 ?
假设 main.ts代码, 和 App.vue 代码如下:
// main.ts
import { createApp } from "vue";
import App from "./App.vue"
createApp(App).mount("#app")
<!-- App.vue -->
<script setup lang="ts">
import Welcome from "./components/Welcome.vue";
</script>
<template>
<div class="box">
<p>Title</p>
<Welcome msg="Hello" :num="999" />
</div>
</template>
如果我们使用 h 函来来实现,代码如下:
import { createApp, defineComponent, h } from "vue"
import Welcome from "./components/Welcome.vue"
const App = defineComponent({
render: () => {
return h("div", { class: "box" }, [
h("p", "Title"),
h(Welcome, { msg: "Hello", num: 999 })
])
}
})
createApp(App).mount("#app")
h 函数用法
使用 h 创建 VNode
h 函数可以创建一个 VNode 实例,可以设置样式和绑定事件,再通过 <component :is="VNode"> 的方式将其动态渲染出来。
<!-- App.vue -->
<script setup lang="ts">
import { h } from "vue";
const com = h(
"div",
{
style: { color: "red" },
onClick: () => {
console.log(1111)
},
},
"Hello World"
)
</script>
<template>
<component :is="com" />
</template>
创建函数式组件
普通函数式组件 (VNode)
刚接触 h 函数时, 我们很容易写出下面的代码:
<!-- App.vue -->
<script setup lang="ts">
import { h, ref } from "vue"
const msg = ref("Hello")
const Com = h("div", { style: { color: "red" } }, msg.value)
setTimeout(() => {
msg.value = "1111"
}, 3000)
</script>
<template>
<component :is="Com" />
</template>
上面的代码中吗,我们的期望效果是:在 3 秒后,通过 ref 改变 msg 的值,页面也会更新。但会发现和我们预期的效果并不一致,三秒后页面并没有发生变化。
⚠️注意:Com 不能写成下面这种形式:
const Com = h("div", { style: { color: "red" } }, msg.value)
因为在 setup 中它不是一个函数式组件,而是一个普通的 VNode,直接在模板中使用时不会触发响应式。我们需要把它改成一个函数式组件。
函数式组件 (响应式)
在 setup 中编写一个函数式组件,在模板中调用这个函数时,就相当于在模板中使用一个响应式组件(能够建立关联关系, 触发 effect )。
<!-- App.vue -->
<script setup lang="ts">
import { h, ref } from "vue"
const msg = ref("Hello")
const Com = () => h("div", { style: { color: "red" } }, msg.value)
setTimeout(() => {
msg.value = "1111"
}, 1000)
</script>
<template>
<component :is="Com" />
</template>
- 上面定义的
Com是一个函数组件,而不是一个VNode。 - 我们在函数调用时访问了
msg.value,而这个函数又是通过<component :is="Com" />动态渲染的,它的调用环境是响应式的。
传递 props
由于 Com 是一个组件,我们还可以给它传递参数:
<!-- App.vue -->
<script setup lang="ts">
import { h } from "vue"
const Coms = (props: { count: number }) => h('div', null, props.count)
</script>
<template>
<Com :count="1"></Com>
</template>
传递 slots
渲染默认插槽
我们可以将 Com 理解为一个 setup 函数,所以可以从第 2 个参数里获取到 slots:
<!-- App.vue -->
<script setup lang="ts">
const Com: FunctionalComponent = (_props, { slots }) =>
h("div", { style: { color: "red" } }, slots)
</script>
<template>
<Com>
<div>Hello World</div>
</Com>
</template>
渲染具名插槽
<!-- App.vue -->
<script setup lang="ts">
const Com: FunctionalComponent = (_props, { slots }) =>
h("div", { style: { color: "red" } }, [
slots?.default?.(),
"Middle Content",
slots?.header?.()
])
</script>
<template>
<Com>
<div>Hello World</div>
<template #header>
<div>header</div>
</template>
</Com>
</template>
渲染效果如下:
渲染作用域插槽(含插值参数)
<script setup lang="ts">
const Com: FunctionalComponent = (_props, { slots }) => {
const num = ref(699)
return h("div", { style: { color: "red" } }, [
slots?.default?.(),
"Vue Content",
slots?.header?.(num.value),
])
}
</script>
<template>
<Com>
<div>Hello World</div>
<template #header="num">
<div>header{{ num }}</div>
</template>
</Com>
</template>
⚠️注意: 上面的代码中,<template #header="num"> 不要写成 <template #header="{ num }">,因为 slots.header传递的是数字,而不是 object:{num: num.value}。
传递属性和事件
<!-- App.vue -->
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue"
const Com = () => {
return h(HelloWorld, {
msg: "666",
onFoo: (text) => {
console.log(text)
}
})
}
</script>
<template>
<Com></Com>
</template>
<!-- ./components/HelloWorld.vue -->
<script setup lang="ts">
defineProps<{ msg: string }>()
const emits = defineEmits(["foo"])
const change = () => emits("foo", "HelloWorld")
</script>
<template>
<div @click="change"> {{ msg }} </div>
</template>
h 渲染组件并传递插槽
我们先来看使用 h 渲染组件时,如果不传递任何内容,会是什么效果:
<!-- App.vue -->
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue"
const Com = () => {
return h(HelloWorld)
}
</script>
<template>
<Com />
</template>
<template>
<div>
<slot>Hello World</slot>
<br />
<slot name="footer" foo="cccc">Footer</slot>
</div>
</template>
最终页面渲染效果如下:
如果我们传入插槽内容,需要注意:此时 h 函数的第三个参数不是数组,而是一个对象:
const Com: FunctionalComponent = () => {
return h(HelloWorld, null, {
default: () => h("div", "aaaa"),
footer: ({ foo }: { foo: string }) => h("div", "bbbb " + foo)
})
}
最终页面渲染效果如下:
当然,我们也可以将 <Com>组件中的插槽内容,传递给 HelloWorld 组件。例如这样写:
<!-- App.vue -->
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue"
const Com: FunctionalComponent = (_props, { slots }) => {
return h(HelloWorld, null, {
default: () => slots.default?.(),
footer: ({ foo }: { foo: string }) => h("div", "bbbb " + foo),
})
}
</script>
<template>
<Com>
<div>Com 组件插槽内容</div>
</Com>
</template>
最终页面渲染效果如下:
一个小技巧
有时候我们可能只想渲染默认插槽,那么 h 函数的第三个参数我们可以简写为直接返回一个函数,因为当 h 函数第三个参数返回一个函数时,会被转换成默认插槽**。**
简写前:
const Com: FunctionalComponent = (_props, { slots }) => {
return h(HelloWorld, null, {
default: () => slots.default?.()
})
}
简写后:
const Com: FunctionalComponent = (_props, { slots }) => {
return h(HelloWorld, null, () => slots.default?.())
}
其他例子:
// 简写前
{ default: () => h(component, { ref: modalInstace, props}) }
// 简写后
() => h(component, { ref: modalInstace, props})
为函数式组件标注 TS 类型
Vue 为我们提供了 FunctionalComponent 来定义函数式组件的 TS 类型:
import type { FunctionalComponent } from 'vue'
const Com: FunctionalComponent = (_props, { slots }) => {
return h(HelloWorld, null, () => slots.default?.())
}
参考链接🔗: 为函数式组件标注类型 | Vue 官方文档
h 和 createVNode 区别
查看 Vue 的源码可以发现,h 函数本质上是对 createVNode 的封装。它的第二个参数名为 propsOrChildren,意味着这个参数既可以用来传递 props,也可以用来传递 children。
因此,h 函数支持多种调用方式,例如:
h("div", h("div", "11111"))
// 等价于
h("div", null, h("div", "11111"))
// 也等价于
h("div", null, [h("div", "11111")])
Vue 源码中的实现如下:
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
const l = arguments.length
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
- 源码链接 🔗: github.com/vuejs/core/…
h 函数的运用场景
上面我们讲了 h 函数到底是什么,以及它的基本用法。我们乘热打铁,继续深入探索 h 函数在实际开发中的几种典型运用场景。
- h 函数二次组件的封装
- 函数式组件封装 (弹窗调用)
- 表格中动态渲染内容
- 封装 HOC 组件
表格中动态渲染内容
在使用 ant-design-vue 渲染表格时,基础代码如下:
<script setup lang="ts">
const columns = [
{ title: "Name",dataIndex: "name" },
{ title: "Address",dataIndex: "address" }
]
const data = [
{ name: "John Brown", address: "New York No. 1 Lake Park" },
{ name: "Jim Green", address: "London No. 1 Lake Park" },
{ name: "Joe Black", address: "Sidney No. 1 Lake Park" }
]
</script>
<template>
<a-table :columns="columns" :data-source="data" bordered></a-table>
</template>
如果我们希望为“名字”加上超链接,可以通过插槽实现:
<script lang="ts" setup>
// columns 和 data 同上
</script>
<template>
<a-table :columns="columns" :data-source="data" bordered>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">
<a href="#">{{ text }}</a>
</template>
</template>
</a-table>
</template>
我们还可以通过 customRender 配合 h 函数实现更灵活的渲染逻辑:
<template>
<a-table :columns="columns" :data-source="data" bordered></a-table>
</template>
<script setup lang="ts">
import { h } from "vue"
const columns = [
{
title: "Name",
dataIndex: "name",
customRender: ({ text }: { text: string }) => {
return h("a", { href: "#" }, text)
}
},
{
title: "Address",
dataIndex: "address",
}
]
// data 同上
</script>
函数式封装组件 (弹窗调用)
在实际开发时,有时我们需要动态创建和挂载组件,例如实现一个弹窗、通知或某些特定的交互组件。以窗组件为例,如果按照传统方式使用弹框组件,我们需在组件内声明
ref、属性及回调函数,若存在多个弹框,会导致代码冗余、命名混乱。
例如:
- 需为每个弹框声明独立
ref控制显示状态 - 点击事件需手动修改
ref值 - 表单提交需通过
ref调用内部方法
什么是函数调用组件?
与直接在模板中定义组件不同,函数调用组件可以动态创建实例,同时便于灵活传参和事件处理。
常见场景:
- 弹窗(如
Dialog、Modal) - 通知组件(如
Toast、MessageBox) - 特定交互(如动态表单、选择器)
核心特点:
- 动态创建:不需要预定义在模板中;
- 灵活传参:支持动态传递
props和事件处理; - 销毁机制:组件可以在合适的时机被清理;
Modal 封装设计思路
通过封装函数实现弹框的动态渲染,核心逻辑如下:
使用函数调用方式
openDialog(Component, { message: '登录提示' }, { title: '用户登录' })
- 第一个参数:弹窗中需要渲染的自定义组件;
- 第二个参数:表单组件的
props数据; - 第三个参数:弹框自身的属性(如
title);
渲染实现步骤
- 使用
h函数创建弹框组件实例; - 将自定义组件通过默认插槽插入弹框组件中;
- 通过
createApp动态创建应用实例并挂载到DOM
动态渲染弹框 (初始版)
import { h, createApp } from 'vue'
import { Modal } from 'ant-design-vue'
const openDialog = (component, componentProps, modalProps) => {
// 创建弹框VNode
const Dialog = () => {
return h(Modal, {
...modalProps,
open: true,
},{
default: () => h(component, componentProps) // 组件插入默认插槽
})
}
// 创建挂载节点
const div = document.createElement('div')
document.body.appendChild(div)
// 动态创建应用并挂载
const app = createApp(Dialog)
app.mount(div)
}
示例表单组件
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue'
import { reactive } from 'vue'
defineProps<{ msg: string }>()
const formRef = ref<FormInstance>()
const formState = reactive({ username: '', password: '' })
async function submit() {
await formRef.value?.validate()
return new Promise(resolve => setTimeout(() => resolve(formState), 2000))
}
defineExpose({ submit })
</script>
<template>
<div>
<h3 class="pb-4 pt-3 font-500">{{ msg }}</h3>
<a-form
ref="formRef"
:model="formState"
name="basic"
autocomplete="off"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
layout="horizontal"
>
<a-form-item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名!' }]"
>
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password v-model:value="formState.password" />
</a-form-item>
</a-form>
</div>
</template>
在页面使用
<script lang="ts" setup>
import { renderDialog } from './dialog'
import LoginForm from './login-from.vue'
function onClick() {
renderDialog(LoginForm, { msg: '晚上好,请登录 👋' }, { title: '登录' })
}
</script>
<template>
<a-button @click="onClick">点击打开登录弹窗</a-button>
</template>
关于组件组册问题
如果你在 mian.ts 是使用全局注册的方式使用组件库,那么上面的代码应该无法熏染 ,因为动态创建的 app 实例需重新注册组件, 如果是使用自动导入插件不需要重新注册。
import Antd from 'ant-design-vue'
const openDialog = (component, componentProps, modalProps) => {
// 在动态应用中注册全局组件
app.use(Antd)
}
处理动画与卸载
直接卸载会导致动画丢失,需通过 afterClose 事件或定时器控制卸载时机:
import { Modal } from 'ant-design-vue'
import { createApp, h, ref } from 'vue'
const openDialog = (component, componentProps, modalProps) => {
// 声明一个 ref 响应式数据,用于控制弹窗的显示与隐藏 (为了保留 Modal 组件关闭动画)
const open = ref(true)
const Dialog = () => {
return h(Modal, {
...modalProps,
open: open.value // 这里不是模板语法!需要 .value
onCancel: () => open.value = false,
// PS: 如果组件库没有 afterClose 钩子,可以使用 setTimeout 处理
afterClose: () => {
// 关闭动画结束后,卸载组件
app.unmount()
document.body.removeChild(div)
},
},{
default: () => h(component, componentProps)
})
}
const div = document.createElement('div')
document.body.appendChild(div)
const app = createApp(Dialog)
app.mount(div)
}
注意: 上面的代码中,Dialog 返回的必须是一个函数,因为响应式数据只有依赖 effect 才能正常工作, 在 Vue 中函数式组件能监听到 ref 响应式数据的变化,所以这里使用函数式组件 。
注意注意再注意!:使用 h 函数必须写成 函数式组件 才能触发响应式
const dialog = h() ❌
const dialog = () => h() ✅
// ❌ 函数触发不了响应式
const NewModal1 = h(Modal, {
open: open.value,
onCancel: () => open.value = false
}, () => h('div', 'Hello World'))
// ✅ 函数式组件可以触发响应式
const NewModal2 = () => h(Modal, {
open: open.value,
onCancel: () => open.value = false
}, () => h('div', 'Hello World 2'))
响应式触发的必要条件(下面两者缺一不可)!
- 数据是响应式的
- 在
effect函数下面执行了get方法(建立了关联关系)
对外导出弹窗销毁方法
有时候我们想在某些逻辑执行后手动销毁弹窗,那我们可以在 openDialog 对外导出一个销毁方法:
import { Modal } from 'ant-design-vue'
import { createApp, h, ref } from 'vue'
const openDialog = (component, componentProps, modalProps) => {
const open = ref(true)
const Dialog = () => {
return h(Dialog, {
...modalProps,
open: open.value
onCancel: () => unmount(900),
},{
default: () => h(component, componentProps)
})
}
const div = document.createElement('div')
document.body.appendChild(div)
const app = createApp(Dialog)
app.mount(div)
// 导出一个对外关闭弹窗的方法,支持外部调用关闭弹窗
const unmount = (delay = 900) => {
if(!open.value) return;
open.value = false
setTimeout(() => {
app.unmount()
document.body.removeChild(div)
}, delay)
}
return { unmount }
}
处理事件 (表单提交与校验)
添加一个 ref 用于接收 弹窗传入的表单组件 实例,在确认按钮点击时手动触发表单组件对外导出的 submit 方法:
import type { ModalProps } from 'ant-design-vue'
import type { Component } from 'vue'
import { Modal } from 'ant-design-vue'
import { createApp, h, ref } from 'vue'
const openDialog = (
component: Component,
componentProps: Record<string, any>,
modalProps: ModalProps
) => {
const open = ref(true)
const instace = ref()
const loading = ref(false)
const Dialog = () => {
return h(Modal, {
...modalProps,
open: open.value
confirmLoading: loading.value,
onCancel: () => unmount(900),
onOk: async () => {
loading.value = true
try {
await instace.value?.submit?.()
}finally {
loading.value = false
}
unmount()
},
},
// { default: () => h(component, { ref: instace, componentProps}) }
// 如果传一个函数,会转换成默认插槽,所以上面代码可以简写为
() => h(component, { ref: instace, componentProps})
)
}
const div = document.createElement('div')
document.body.appendChild(div)
const app = createApp(Dialog)
app.mount(div)
// 导出一个对外关闭弹窗的方法,支持外部调用关闭弹窗
const unmount = (delay = 900) => {
if(!open.value) return
open.value = false
setTimeout(() => {
app.unmount()
document.body.removeChild(div)
}, delay)
}
// 对外导出卸载方法和表单组件实例
return { unmount, instace }
}
封装拓展
如果考虑自定义的提交事件怎么办 ?
const {methodKey = 'submit' } = props
if(instace.value?.[methodKey]){
try {
await instace.value?.[methodKey]?.()
}finally {
loading.value = false
}
}
import type { ModalProps } from 'ant-design-vue'
import type { Component } from 'vue'
import Antd, { Modal } from 'ant-design-vue'
import { createApp, defineComponent, h, reactive, ref } from 'vue'
const EMPTY_OBJ = Object.freeze({})
export function renderDialog(
component: Component,
props: Record<string, any> = EMPTY_OBJ,
modalProps: ModalProps = EMPTY_OBJ,
) {
const { methodKey = 'submit', onSubmit } = props
const open = ref(true)
const instance = ref()
const isLoading = ref(false)
const _modalProps: ModalProps = {
...modalProps,
async onOk(...args) {
/** 传了自定义的 onSubmit 方法 */
if (onSubmit) {
await onSubmit(instance.value)
}
else if (instance.value?.[methodKey]) {
/** 传了 methodKey */
isLoading.value = true
await instance.value[methodKey]()
.finally(() => isLoading.value = false)
}
open.value = false
modalProps.onOk?.(...args)
},
onCancel(...args) {
open.value = false
modalProps.onCancel?.(...args)
},
afterClose(...args) {
unmount()
modalProps.afterClose?.(...args)
},
}
// 这里使用 reactive 是为了特殊需求(比如弹窗显示 2秒后动态更改props数据:如标题)
const reactiveModalProps = reactive(modalProps)
const dialog = defineComponent({
setup: () => {
return h(
Modal,
{
...reactiveModalProps,
..._modalProps,
open: open.value,
confirmLoading: isLoading.value,
},
() => h(component, { ...props, ref: instance }),
)
},
})
const app = createApp(dialog)
app.use(Antd) // 如果是全局注册需要重新注册,使用自动导入则不需要
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
function unmount() {
document.body.removeChild(div)
app.unmount()
}
}
如何共享 Vue app 实例上下文 ?
1. 暴力模式
在 main.ts 中
const app = createApp(App)
window._APP_CONTEXT = app._context
app.mount('#app')
使用
const app = createApp(Dialog)
app._context = window._APP_CONTEXT
// app._context.provides = window._APP_CONTEXT.provides
2. 重写 createApp
我们还可以重写一下 createApp 方法,在需要支持共享 App 的实例方法的时候,直接使用我们自己重写的 createApp 方法:
import type { App } from 'vue'
import Antd from 'ant-design-vue'
import { createPinia } from 'pinia'
import { createApp as _createApp } from 'vue'
import router from '~/router'
function loadPlugins(app: App) {
app.use(Antd)
app.use(createPinia())
app.use(router)
}
export const createApp: typeof _createApp = (...args) => {
const app = _createApp(...args)
loadPlugins(app)
return app
}
element plus 版本
import type { Component, ComponentPublicInstance } from 'vue'
import { ElButton, ElDialog } from 'element-plus'
import { createApp, h, ref } from 'vue'
/**
* 弹窗渲染器
* @param component 要渲染的组件
* @param props 组件的 props
* @param modalProps 弹窗的属性(ElDialog 的 props)
*/
export function renderDialog<
T extends Record<string, any> = Record<string, any>,
I extends ComponentPublicInstance<{ submit?: () => Promise<any> }> = ComponentPublicInstance,
>(
component: Component<T>,
props?: T,
modalProps?: Partial<InstanceType<typeof ElDialog>['$props']>,
): void {
const open = ref(true)
const loading = ref(false)
const instance = ref<I>()
const dialog = () =>
h(
ElDialog,
{
...modalProps,
'modelValue': open.value,
'onUpdate:modelValue': (val: boolean) => {
open.value = val
},
onClosed() {
app.unmount()
document.body.removeChild(div)
},
},
{
default: () => h(component, { ...(props as T), ref: instance } satisfies T & { ref: typeof instance }),
footer: () =>
h('div', { class: 'dialog-footer' }, [
h(ElButton, { onClick: cancel }, () => '取 消'),
h(
ElButton,
{ type: 'primary', onClick: submit, loading: loading.value },
() => '确 认',
),
]),
},
)
const app = createApp(dialog)
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
async function submit() {
loading.value = true
try {
await instance.value?.submit?.()
open.value = false
}
finally {
loading.value = false
}
}
function cancel() {
open.value = false
}
}
更复杂的案例参考链接 🔗:
使用 h 函数封装 HOC 函数
1. 什么是 HOC ?
高阶组件 HOC 在 React 社区是非常常见的概念,先来回顾一下在 React 里⾼阶组件是什么?
function withExtraProp(Component) {
return function(props) {
return <Component extraProp="someValue" {...props} />
}
}
- HOC本质上是⼀个函数,它接收⼀个组件作为参数,然后返回⼀个新的组件。
- 返回的新组件将拥有被包裹的组件的所有
props,并且可以添加额外的props或状态。
高阶组件的业务场景:
- 授权和权限管理:返回的新的组件在渲染之前会检查⽤户是否已经认证。
- 数据获取: 新的组件在挂载时获取数据,并将数据通过 props 传递给被包裹的组件。
- 错误处理:在发⽣错误时显示错误信息或其他备⽤内容。
2. 从实际业务场景出发
有一天产品找到你,说要给我们的系统增加会员功能,需要让系统中的几十个功能块增加会员可见功能。如果不是会员这几十个功能块都显示成引导用户开通会员的UI,并且这些功能块涉及到几十个组件,分布在系统的各个页面中。
你肯定很容易想到将会员相关的功能抽取成一个名为 useVip.ts 的组合式函数(hook),代码如下:
export function useVip() {
function getShowVipContent() {
// 一些业务逻辑判断是否是VIP
return false;
}
return {
isVip: getShowVipContent()
}
}
然后再去每个具体的业务模块中去使用 isVip 变量判断,并大量使用 v-if="isVip" 显示原模块,v-else显示引导开通会员 UI。代码如下:
<template>
<Content v-if="isVip" :userInfo="userInfo" @update="onUpdateProfile" />
<ActivateVipTip v-else />
</template>
<script setup lang="ts">
import Content from "./content.vue"
import ActivateVipTip from "./activate-vip-tip.vue"
import { useVip } from "@/hooks/useVip"
const userInfo = ref({ name: 'Yi', age: 24 })
const { isVip } = useVip()
const updateProfile = () => {}
</script>
- 如果页面中有几十个地方需要修改呢,不麻烦吗 ?
- 你需要修改的地方都是自己写的业务逻辑吗,不会出错吗 ?
- 如果再加一个 SVIP 功能、或者企业级功能单独显示的 UI 呢 ?
上面的这一场景使用 hooks 去实现,虽然能够完成,但是因为入侵了这几十个模块的业务逻辑。所以容易出错,也改起来比较麻烦,代码也不优雅。
那么有没有一种更好的解决方案,让我们可以不入侵这几十个模块的业务逻辑的实现方式呢 ?
答案是:高阶组件HOC。
3. Vue 中的组件即对象
我写了一个组件如下:
<script setup lang="ts">
defineProps({ msg: String })
</script>
<template>
<div>
{{ msg }}
<slot />
</div>
</template>
我们在父组件引入这个组件,并且打印一下这个子组件:
<script setup lang="ts">
import { ref } from 'vue'
import Comp from './Comp.vue'
console.log(Comp);
</script>
如下图所示, Vue 中的组件经过编译后其实就是一个对象,对象中的 props 属性对应的就是我们写的 defineProps, 而对象中的 setup 方法,对应的就是我们熟知的 <script setup> 语法糖。
还有一点,在 Vue 中,如果在 setup 方法中返回一个函数,那么在 Vue 内部就会认为这个函数就是实际的 render 函数,并且在 setup 方法中我们天然的就可以访问定义的变量。
实战:实现一个简单的HOC
HOC 的一个用途就是对组件进行增强,并且不会入侵原有组件的业务逻辑,在这里就是使用 HOC 判断会员相关的逻辑,如果是会员那么就渲染原本的模块组件,否则就渲染引导开通 VIP 的 UI。
基于在 setup 方法中返回一个函数的方式,我们可以在 Vue3 中实现一个简单的高阶组件,代码如下:
import { h } from "vue"
import ActivateVipTip from "./activate-vip-tip.vue"
import type { Component } from 'vue'
function getShowVipContent() {
// ... 验证是否为会员的逻辑
return true
}
export function WithVip(Comp: Component) {
return {
setup() {
const isVip = getShowVipContent()
return () => {
return isVip ? h(Comp) : h(ActivateVipTip)
}
}
}
}
<script lang="ts" setup>
import Content from './content.vue'
import { WithVip } from './with-vip'
const VipContent = WithVip(Content)
</script>
<template>
<VipContent />
</template>
这个代码相比前面的 hooks 的方案就简单很多了,只需要使用高阶组件 WithVip 对原来的 Content 组件包一层,然后将原本使用 Content 的地方改为使用 VipContent,对原本的代码基本没有入侵。
HOC 更复杂的例子参考文章: