h
到底是个啥?
对于了解或学习Vue高阶组件(HOC)的同学来说,h()
函数无疑是一个经常遇到的概念。
那么,这个h()
函数究竟如何使用呢,又在什么场景下适合使用呢?
一、h 是什么
看到这个函数你可能会有些许困惑,为什么叫h呢?代表着什么呢?
官方定义:
返回一个“虚拟节点” ,通常缩写为 VNode: 一个普通对象,其中包含向 Vue 描述它应该在页面上呈现哪种节点的信息,包括对任何子节点的描述。用于手动编写render
h
其实代表的是 hyperscript 。它是 HTML 的一部分,表示的是超文本标记语言,当我们正在处理一个脚本的时候,在虚拟 DOM 节点中去使用它进行替换已成为一种惯例。这个定义同时也被运用到其他的框架文档中
Hyperscript 它本身表示的是 “生成描述 HTML 结构的脚本”
二、语法
h()
函数的基本语法如下:
h(tag, props, children)
- tag:可以是字符串或组件,表示要创建的 HTML 标签或 Vue 组件。
- props:是一个对象,包含要传递给标签或组件的属性,如类名、样式、事件监听器等。
- children:可以是字符串、数组或函数,表示子节点。子节点可以是文本、其他 VNode 或一个返回 VNode 的函数。
以下是一个简单的代码示例,展示了如何使用 h()
函数创建一个包含标题和段落的组件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>h()</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.4/vue.global.js"></script>
</head>
<body>
<div id="app">
</div>
<script>
const App = {
render() {
return Vue.h('h1', {}, '二川兄弟')
}
}
// console 结果请在控制台查看
console.log(Vue.h('h1', {}, '二川兄弟'))
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
效果:
打印:
在这个示例中,我们创建了一个 <h1>
容器,内部插入文字 “二川兄弟”
。所有这些都是通过 h()
函数来实现的,而不是使用模板语法。
三、源码解析
要深入理解 h()
函数,我们需要查看 Vue.js 的源码。在 Vue.js 的实现中,h()
函数是一个封装了 VNode 创建逻辑的工具函数。它接收标签名、属性和子节点作为参数,并返回一个包含这些信息的 VNode 对象。
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
if (arguments.length === 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 (isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
VNode 是 Vue.js 对真实 DOM 的一种轻量级表示。它包含了节点的类型、属性、子节点等信息,但不包含具体的 DOM 元素。Vue.js 会在渲染时将 VNode 转换为真实的 DOM 元素。
在 h()
函数的源码中,你会看到大量的边界情况处理和类型检查。例如,如果子节点是一个字符串,它会被转换为一个文本节点;如果子节点是一个数组,它会遍历数组并递归地创建子节点的 VNode;如果子节点是一个函数,它会调用该函数并传递当前上下文来创建子节点的 VNode。
_createVNode 又做了啥?
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
// 更新标志
patchFlag: number = 0,
// 自定义属性
dynamicProps: string[] | null = null,
// 是否是动态节点,(v-if v-for)
isBlockNode = false
): VNode {
// type必传参数
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// Class 类型的type标准化
// class component normalization.
if (isFunction(type) && '__vccOpts' in type) {
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// props 如果是响应式,clone 一个副本
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
// 标准化class, 支持 string , array, object 三种形式
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
// 标准化style, 支持 array ,object 两种形式
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`,
`\nComponent that was made reactive: `,
type
)
}
// 构造 VNode 模型
const vnode: VNode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
suspense: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
}
normalizeChildren(vnode, children)
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
// patchFlag 标志存在表示节点需要更新,组件节点一直存在 patchFlag,因为即使不需要更新,它需要将实例持久化到下一个 vnode,以便以后可以正确卸载它
if (
shouldTrack > 0 &&
!isBlockNode &&
currentBlock &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
patchFlag !== PatchFlags.HYDRATE_EVENTS &&
(patchFlag > 0 ||
shapeFlag & ShapeFlags.SUSPENSE ||
shapeFlag & ShapeFlags.TELEPORT ||
shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
) {
// 压入 VNode 栈
currentBlock.push(vnode)
}
return vnode
}
四、使用场景
h()
函数在 Vue.js 中有多种使用场景,以下是一些常见的场景:
-
渲染函数:
- 当需要完全控制组件的渲染逻辑时,可以使用渲染函数,并在其中使用
h()
函数来创建 VNode。这种方式提供了比模板语法更高的灵活性和控制力。
以下是一个使用渲染函数的示例,展示了如何根据条件动态渲染不同的内容:
import { h } from 'vue'; export default { props: ['isLoggedIn'], render() { return h('div', {}, this.isLoggedIn ? h('p', {}, 'Welcome back!') : h('p', {}, 'Please log in.') ); } };
- 当需要完全控制组件的渲染逻辑时,可以使用渲染函数,并在其中使用
-
高阶组件(HOC):
- 高阶组件是一种模式,它接收一个组件作为参数,并返回一个新的组件。在创建新组件的过程中,
h()
函数用于定制或扩展原始组件的渲染逻辑。
以下是一个高阶组件的示例,展示了如何为组件添加额外的类名:
function withClassName(WrappedComponent, className) { return { props: WrappedComponent.props, render() { return h(WrappedComponent, { ...this.$props, class: className }); } }; } // 使用高阶组件 const MyComponentWithClass = withClassName(MyComponent, 'my-custom-class');
- 高阶组件是一种模式,它接收一个组件作为参数,并返回一个新的组件。在创建新组件的过程中,
-
动态组件:
- 在需要根据条件动态渲染不同组件时,
h()
函数可以方便地根据条件创建不同的 VNode。
以下是一个动态组件的示例,展示了如何根据条件渲染不同的组件:
import { h, defineComponent } from 'vue'; import ComponentA from './ComponentA.vue'; import ComponentB from './ComponentB.vue'; export default defineComponent({ props: ['condition'], render() { return this.condition ? h(ComponentA, { ...this.$props }) : h(ComponentB, { ...this.$props }); } });
- 在需要根据条件动态渲染不同组件时,
-
手动创建复杂结构:
- 在某些情况下,可能需要手动创建复杂的组件结构,而不是使用模板语法。这时,
h()
函数就显得非常有用。
以下是一个手动创建复杂结构的示例,展示了如何创建一个带有嵌套子组件的表格:
import { h } from 'vue'; import TableRow from './TableRow.vue'; export default { props: ['data'], render() { return h('table', {}, this.data.map(row => h(TableRow, { key: row.id, row: row }) ) ); } };
- 在某些情况下,可能需要手动创建复杂的组件结构,而不是使用模板语法。这时,
五、其他比较
与模板语法相比,使用 h()
函数提供了更高的灵活性和控制力。模板语法更适合于简单的组件和静态内容,而 h()
函数则更适合于复杂的组件和动态内容。
此外,与 JSX 相比,h()
函数更加简洁和直接。JSX 是一种语法糖,它允许在 JavaScript 中编写类似 HTML 的代码。然而,JSX 需要额外的编译步骤,并且可能会增加代码的复杂性。而 h()
函数则是 Vue.js 内置的工具函数,无需额外的编译步骤,并且更加符合 Vue.js 的设计哲学。
六、最佳实践
在使用 h()
函数时,以下是一些最佳实践:
-
保持简洁:
- 尽量避免在渲染函数中编写过多的逻辑。可以将复杂的逻辑拆分成多个函数或组件,以提高代码的可读性和可维护性。
-
使用辅助函数:
- 可以编写一些辅助函数来简化
h()
函数的使用。例如,可以编写一个函数来创建带有特定样式的节点,或者一个函数来创建带有事件监听器的节点。
- 可以编写一些辅助函数来简化
-
避免过度使用:
- 在大多数情况下,模板语法已经足够满足需求。只有在需要更高的灵活性和控制力时,才应该考虑使用
h()
函数。
- 在大多数情况下,模板语法已经足够满足需求。只有在需要更高的灵活性和控制力时,才应该考虑使用
-
与模板语法结合使用:
- 可以将
h()
函数与模板语法结合使用。例如,可以在模板中使用<script setup>
语法来定义渲染函数,并在其中使用h()
函数来创建复杂的节点结构。
- 可以将
七、总结
h()
函数是 Vue.js 框架中一个强大且灵活的工具,用于在渲染函数中创建虚拟 DOM 节点。通过深入理解 h()
函数的语法、源码和使用场景,开发者可以更好地掌握 Vue.js 的渲染机制,并在实际开发中灵活运用这一工具来创建高效、可维护的组件。无论是编写渲染函数、实现高阶组件,还是处理动态组件和复杂结构,h()
函数都是不可或缺的一部分。