本片文章,会从h函数引入,逐步了解到h、createVNode、 cloneVNode、 mergeProps、isVNode 等高阶API的使用方式及原理。
渲染函数h()
在Vue2中,有个全局API:render函数。Vue内部回给这个函数传递一个h函数,用于创建Vnode的描述对象。
这次,在Vue3中。将h函数独立出来,作为一个单独的API,它的作用仍保持原样:用于创建一个描述所渲染节点的Vnode描述对象。
可以接受三个参数: type、props、children。
type用于表示Vnode节点类型,可以是HTML标签名、组件、异步组件或函数式组件。使用返回null的函数将渲染一个注释,此参数必传。props是一个对象,与我们将在模板中使用的attribute、prop和事件相对应。可选。children是子节点VNode,使用h()生成,或者使用字符串来获取“文本VNode”,或带有插槽的对象。可选。
在刚开始学习Vue的时候,我一直搞不懂render函数中h的使用方式。如果你也是一直通过HTML模板语法来搭建页面结构,可能也会对h函数不特别熟悉,下面可以一起学习下。
当我们创建一个组件时,一般都是通过HTML模板来描述UI部分,比如:
- 使用
HTML标签:
<template>
<div>
<input
type="radio"
:id="branch"
:value="branch"
name="branch"
v-model="currentBranch" />
<label :for="branch">{{ branch }}</label>
</div>
</template>
- 使用自定义组件标签:
<template>
<tree-item class="item" :model="treeData" @chang="changeHandler"></tree-item>
</template>
其实这些都可以将通过JS抽象为三部分,并用对象描述:
- 用于表示模板标签类型的
type - 传给模板的
attribute、prop和事件 - 标签包裹的子节点
children
且子节点同样可以抽象为同样的结构。
而h函数就是做了这么一件事。给他传入type、props、children。它返回对应的Vnode描述对象。
可不可以直接创建一个Vnode描述对象
当然可以,只不过如果涉及Vnode的描述全部自己写的话,有点太累,而且容易出错。
我们先看下Vue内部定义的Vnode对象所包含的属性:
__v_isVNode: *true*,内部属性,有该属性表示为Vnode__v_skip: true,内部属性,表示跳过响应式转换,reactive转换时会根据此属性进行判断isCompatRoot?: *true*,用于是否做了兼容处理的判断type: VNodeTypes,虚拟节点的类型props: (VNodeProps & ExtraProps) | *null*,虚拟节点的propskey: *string* | *number* | *null*,虚拟阶段的key,可用于diffref: VNodeNormalizedRef | *null*,虚拟阶段的引用scopeId: *string* | *null*,仅限于SFC(单文件组件),在设置currentRenderingInstance当前渲染实例时,一期设置slotScopeIds: *string*[] | *null*,仅限于单文件组件,与单文件组件的插槽有关children: VNodeNormalizedChildren,子节点component: ComponentInternalInstance | null,组件实例dirs: DirectiveBinding[] | null,当前Vnode绑定的指令transition: TransitionHooks<HostElement> | null,TransitionHooksDOM相关属性el: HostNode | *null*,宿主阶段anchor: HostNode | *null* // fragment anchortarget: HostElement | *null*,teleport target传送的目标targetAnchor: HostNode | *null* // teleport target anchorstaticCount: *number*,包含的静态节点的数量
suspense悬挂有关的属性-
suspense: SuspenseBoundary | *null* -
ssContent: VNode | *null* -
ssFallback: VNode | *null*
-
optimization only用于优化的属性shapeFlag: *number*patchFlag: *number*dynamicProps: *string*[] | *null*dynamicChildren: VNode[] | *null*
- 根节点会有的属性
appContext: AppContext | *null*,实例上下文
可以看到在Vue内部,对于一个Vnode描述对象的属性大概有二十多个,有些属性还必须经过规范梳理。
Vue为了给用于减轻一定的负担,但又不至于太封闭,就创建了渲染h。可以在用户需要的时候,通过h函数创建对应的Vnode即可。
这样就给为一些高阶玩家保留了自由发挥的空间。
那为什么要使用h函数呢?
其实官方文档已经给出了一个非常贴切又简单的实例,👉传送门:渲染函数
通过官方示例,可以知道。
javascript相较于模板语法,有更高的自由度。当使用模板太过臃肿的时候,比如多个if/else,就可以使用渲染函数h。
如何用
v-if
<span v-if="user">
{{user.name}}
</span>
<p v-else>Plase login.</p>
使h函数表述如下:
render() {
return this.user ? h('span', null, user.name) : h('p', 'Plase login.')
}
从上面代码可以知道:
- 可以通过三元运算符代替
v-if/v-else指令 - 或者通过
if/else代替v-if/v-else指令
v-show
<div v-show="isActive">Content</div>
使h函数表述如下:
render() {
return h("div", {
"directives": [{
name: "show",
value: isActive
}],
}, "Content");
}
v-for
<ul>
<li v-for="item in items">{{ item.name }}</li>
</ul>
使h函数表述如下:
render() {
return h('ul', this.items.map((item) => {
return h('li', item.name)
}))
}
- 可以通过map函数代替v-for指令
- 通过map返回的Vnode,每一个都是不同的对象
v-on
<button @click="onClick">Button</button>
使h函数表述如下:
render() {
return h('button', {
onClick: onClick
})
}
对于input标签可以通过
-
onBlur监听失去焦点事件 -
onFocus监听焦点事件 -
onInput监听输入事件 -
onClick监听点击事件 -
onKeypress监听键盘事件
v-model
在Vue中,我们可以通过v-bind由上向下传值。
也可以通过v-model由上向下传值。
当使用v-model时,其本质时v-bind与v-on的语法糖;
在h函数中,如何表示v-model?我们看下代码:
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(Component, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
上面的代码是一个官方示例。这里表示的是:
- 但使用
v-model绑定value时。必须给子组件props中绑定一个value,及一个监听更新的函数,来代替v-bind与v-on。
attrs
在英文中props与attrs都代表属性的含义,但在Vue中这两个属性含义却不相同:
props表示元素对象的属性attrs表示元素标签的属性
比如当我们调用h函数创建Vnode时,传递的第二个参数,就是Vnode对象的属性。
而当我们需要给元素标签设置attrs时该如何做呢?
<input type="button" disabled="true"/>
使h函数表述如下:
render() {
return h(input, {
"attrs": {
type: button,
disabled: true
}
})
}
由此在h函数中可见props包含attrs。
v-slot
在Vue中slot为模板提供了内容分发能力。
在使用时,只需要使用slot标签进行占位就可以。
下面看下如何使用h函数创建插槽。
<div><slot></slot></div>
使h函数表述如下:
普通插槽
可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() {
return h('div', {}, this.$slots.default())
}
作用域插槽:
<!--定义插槽组件child-->
<div><slot :text="message"></slot></div>
<!--使用组件child-->
<div><child v-slot:default="slotProps">{{ slotProps.text }}</child></div>
render() {
return h('div', {}, this.$slots.default({
text: this.message
}))
}
- 可以通过
this.$slot访问静态插槽的内容 - 如果需要传递状态,可以给
this.$slots.default()函数传递一个对象参数
自定义组件
<div><child v-slot:default="slotProps"><span>{{ slotProps.text }}</span></child></div>
const { h, resolveComponent } = Vue
render() {
return h('div', [
h(
resolveComponent('child'),
{},
// 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
{
default: (props) => Vue.h('span', props.text)
}
)
])
}
resolveComponent API会返回child组件的Vnode。
动态组件
<component :is="name"></component>
使h函数表述如下:
const { h, resolveDynamicComponent } = Vue
render() {
const Component = resolveDynamicComponent(this.name)
return h(Component)
}
使用动态组件时,可以使用resolveDynamicComponent代替is attribute,resolveDynamicComponent支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。
能给is传什么就能给resolveDynamicComponent传什么。
内置组件
在Vue中, KeepAlive, Teleport, Transition, TransitionGroup等通被称为Vue内置组件,内置组件与用户自定义组件不同的是,内置组件没有进行局部或者全局注册,所以无法通过resolveComponent去访问他们。在使用h函数时,需要自信导入:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
return h(Transition, { mode: 'out-in' }, /* ... */)
}
ref
<someComponent ref="someComponent"></someComponent>
使h函数表述如下:
render() {
return h(someComponent, {"ref": "someComponent"}))
}
自定义指令
可以使用 withDirectives 将自定义指令应用于VNode:
const { h, resolveDirective, withDirectives } = Vue
// <div v-pin:top.animate="200"></div>
render () {
const pin = resolveDirective('pin')
return withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
}
resolveDirective API 是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。
后面我们会对withDirectives API进行分析。
resolveComponent API及resolveDirective API原理是一样的。当我们创建一个组件的时候,给这个组件配置compontes属性、directives属性。这些属性最终都会绑定在实例上,resolve组件/指令的过程,就是通过访问当前实例的compontes/directives属性的过程。
渲染函数h()源码分析
这部分内容可能比较枯燥,主要就是分析
h函数是如何创建Vnode,创建过程中会做哪些处理。
Vnode就是一个虚拟节点的普通JS对象,Vue会根据对象信息,渲染对应的节点。
Vnode类型
可以给h函数传递的Vnode节点类型:
stringVNodeComponentTextStaticCommentFragmentTeleportImplSuspenseImpl
直接看下源码:
function h(type, propsOrChildren, children) {
// 根据参数长度判断是否有子节点
const l = arguments.length
if (l === 2) {
// 👉传两个参数
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 👉propsOrChildren 是对象且不是数组时
if (isVNode(propsOrChildren)) {
//👉props是Vnode类型,则propsOrChildren为子节点
return createVNode(type, null, [propsOrChildren])
}
// 👉props不包含子节点
return createVNode(type, propsOrChildren)
} else {
// 👉省略props
return createVNode(type, null, propsOrChildren)
}
} else {
// 👉当存在2个已上的参数时
// 👉将子节点放入children数组中
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
通过上面代码知道,渲染函数h只是createVnode函数的语法糖。
渲染h()函数的主要职责就是通过判断参数的长度和类型,去调用createVnode函数创建Vnode。
下面看下createVnode函数。
createVNode
createVnode函数位于Vue源码的runtime-core中vnode.ts文件夹。
createVNode 其实还是调用的_createVNode。
这里暂时不用关注
vnodeArgsTransformer。
export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode)
const createVNodeWithArgsTransform = (...args) => {
return _createVNode(
...(vnodeArgsTransformer
? vnodeArgsTransformer(args, currentRenderingInstance)
: args)
)
}
_createVNode
- 首先进行类型校验,如果不符合预期,在
dev环境会警告,prod环境会作为注释节点类型。 - 在判断是否已经是
Vnode,是的话直接克隆节点,并对自己点进行规范梳理。 - 如果是类组件,会获取
__vccOpts - 做Vue2的异步或者函数组件的兼容
- 如果
props存在,会对props进行判断,并规范我们传给节点的class、style,会将class处理为字符串,将style处理为对象 - 创建
Vnode - 规范梳理子节点
- 如果构建时需要做兼容处理,则做
Vue2的兼容处理,最后返回虚拟节点
function _createVNode(
type,
props,
children,
patchFlag,
dynamicProps,
isBlockNode = false
){
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 👉如果type是Vnode类型,则克隆这个类型的节点,规范梳理子节点,返回克隆的节点
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// 👉如果时类组件类型
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 👉兼容Vue2的处理
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
//👉 if块中主要处理 class & style 属性
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
// 如果props是响应式对象,需要通过Object.assign进行拷贝
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
// class不是字符串,需要规范为字符串
props.class = normalizeClass(klass)
}
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)
}
}
// 将vnode类型信息转为 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)) {
// 省略...
)
}
//👉 创建VNode的描述对象
const vnode: VNode = {
__v_isVNode: true, // 👉标识 该对象为虚拟节点
__v_skip: true, // 👉标识 该对象跳过proxy
type, // 类型
props,
key: props && normalizeKey(props), // 👉梳理props
ref: props && normalizeRef(props),// 👉梳理ref
scopeId: currentScopeId,
slotScopeIds: null,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null, // 动态子节点
appContext: null // 实例上下文
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// 👉规范子节点
normalizeChildren(vnode, children)
// 👉如果时suspense类型虚拟DOM,规范 suspense 子节点
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type).normalize(vnode)
}
// 这里暂时不关注
if (
isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
// 👉兼容处理
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
convertLegacyRefInFor(vnode)
defineLegacyVNodeProperties(vnode)
}
// 👉返回虚拟节点
return vnode
}
通过上面的代码可以看出,_createVNode函数的主要职责:
- 梳理规范
props中的class、style、child - 创建
Vnode的描述对象,并返回 - 对
Vue2做兼容处理
Object.assign与Proxy:stackoverflow.com/questions/4…
上面代码中,如果type是Vnode类型,会调用cloneVNode创建克隆的节点,接下来我们看下cloneVNode函数。
cloneVNode
其实我们可以先思考一下,克隆一个Vnode,其实可以简化为克隆一棵tree,是一个递归克隆的过程。
export function cloneVNode(
vnode,
extraProps,
mergeRef = false
){
// This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost.
const { props, ref, patchFlag, children } = vnode
// 👉合并props
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
// 👉创建Vnode克隆对象
const cloned = {
__v_isVNode: true,
__v_skip: true,
type: vnode.type,
props: mergedProps,
key: mergedProps && normalizeKey(mergedProps),
ref:
extraProps && extraProps.ref
mergeRef && ref
? isArray(ref)
? ref.concat(normalizeRef(extraProps)!)
: [ref, normalizeRef(extraProps)!]
: normalizeRef(extraProps)
: ref,
scopeId: vnode.scopeId,
slotScopeIds: vnode.slotScopeIds,
children:
__DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children)
? children.map(deepCloneVNode) // 对子节点进行深克隆
: children,
target: vnode.target,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
patchFlag:
extraProps && vnode.type !== Fragment
? patchFlag === -1 // hoisted node
? PatchFlags.FULL_PROPS
: patchFlag | PatchFlags.FULL_PROPS
: patchFlag,
dynamicProps: vnode.dynamicProps,
dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
dirs: vnode.dirs,
transition: vnode.transition,
component: vnode.component,
suspense: vnode.suspense,
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
el: vnode.el,
anchor: vnode.anchor
}
// 兼容处理
if (__COMPAT__) {
defineLegacyVNodeProperties(cloned)
}
return cloned
}
cloneVNode主要做了这么几件事:
- 合并props
- 创建克隆对象
- 对Vnode子节点进行深度克隆
deepClone
深度克隆, 如果子节点是数组类型会进行递归克隆。
function deepCloneVNode(vnode) {
const cloned = cloneVNode(vnode)
if (isArray(vnode.children)) {
cloned.children = vnode.children.map(deepCloneVNode)
}
return cloned
}
isVNode
很简单,根据创建Vnode描述对象时的私有属性判断。
export function isVNode(value) {
return value ? value.__v_isVNode === true : false
}
normalizeChildren
在_createVNode中,我们知道,如果Vnode纯在子节点的时候,会执行normalizeChildren规范梳理子节点。下面看下normalizeChildren是如何做的:
export function normalizeChildren(vnode, children) {
let type = 0
const { shapeFlag } = vnode
// 👉下面会对children类型做判断,不同类型,不同操作
if (children == null) {
// 👉children 是null
children = null
} else if (isArray(children)) {
// 👉children 是数组,标记type为ARRAY_CHILDREN
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
// 👉children 是对象
if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
// Normalize slot to plain children for plain element and Teleport
const slot = (children).default
if (slot) {
// _c marker is added by withCtx() indicating this is a compiled slot
slot._c && (slot._d = false)
normalizeChildren(vnode, slot())
slot._c && (slot._d = true)
}
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children)._
if (!slotFlag && !(InternalObjectKey in children!)) {
;(children)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
if (
(currentRenderingInstance.slots)._ === SlotFlags.STABLE
) {
;(children)._ = SlotFlags.STABLE
} else {
;(children)._ = SlotFlags.DYNAMIC
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
}
}
} else if (isFunction(children)) {
// 👉children是函数
children = { default: children, _ctx: currentRenderingInstance }
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children)
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children)]
} else {
type = ShapeFlags.TEXT_CHILDREN
}
}
vnode.children = children
vnode.shapeFlag |= type
}
从上面代码可以看出,normalizeChildren主要对Vnode.children与type做了规范梳理。
isClassComponent
export function isClassComponent(value) {
return isFunction(value) && '__vccOpts' in value
}
normalizeStyle
当我们给组件绑定style的时候,可能回这么写:
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
通过对象语法动态绑定style。
也可能这么写:
<div :style="[baseStyles, overridingStyles]"></div>
data() {
return {
baseStyles: {
activeColor: 'red',
fontSize: 30
},
overridingStyles: {
display: flex
},
}
}
通过数组给元素绑定多个style对象。
但是这两种写法。最终都会通过normalizeStyle函数进行规范梳理。
下面看下normalizeStyle函数:
export function normalizeStyle(value) {
if (isArray(value)) {
const res = {}
for (let i = 0; i < value.length; i++) {
const item = value[i]
const normalized = normalizeStyle(
isString(item) ? parseStringStyle(item) : item
)
if (normalized) {
for (const key in normalized) {
res[key] = normalized[key]
}
}
}
return res
} else if (isObject(value)) {
return value
}
}
normalizeStyle函数很简单,通过遍历递归将数组类型的value,规范为对象类型并返回。
normalizeClass
在我们给节点绑定类的时候,基本有三种形式:
- 以字符串形式绑定
- 以对象形式绑定
- 以数组形式绑定
但最终绑定到节点上的class,都会以string处理,normalizeClass做的就是这件事。
将所有非string的形式链接为string。
export function normalizeClass(value) {
let res = ''
if (isString(value)) {
res = value
} else if (isArray(value)) {
// 👉遍历递归处理
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized) {
res += normalized + ' '
}
}
} else if (isObject(value)) {
// 👉将对象转化为string
for (const name in value) {
if (value[name]) {
res += name + ' '
}
}
}
return res.trim()
}
normalizeClass函数思路其实与normalizeStyle相同。
Tip:这种遍历递归经常会在面试题中出现。
mergeProps
在前面的分析中,我们知道,克隆Vnode的过程中,回调用mergeProps对vnode.props进行合并。并将合并后的mergedProps传给cloned Vnode。
下面看下mergedProps是如何进行合并的?
export function mergeProps(...args) {
const ret = extend({}, args[0])
for (let i = 1; i < args.length; i++) {
const toMerge = args[i]
for (const key in toMerge) {
if (key === 'class') {
// 👉merge Class
if (ret.class !== toMerge.class) {
ret.class = normalizeClass([ret.class, toMerge.class])
}
} else if (key === 'style') {
// 👉merge Style
ret.style = normalizeStyle([ret.style, toMerge.style])
} else if (isOn(key)) {
// 👉merge 监听的事件
const existing = ret[key]
const incoming = toMerge[key]
if (existing !== incoming) {
ret[key] = existing
? [].concat(existing, incoming)
: incoming
}
} else if (key !== '') {
ret[key] = toMerge[key]
}
}
}
return ret
}
- 会对节点的
class、style、绑定的事件及非空属性进行合并 - 合并的过程会对
class、style做normalize处理 - 如果绑定多个事件,会将所有事件存储在数组中
withDirectives
function withDirectives(vnode, directives) {
const internalInstance = currentRenderingInstance
if (internalInstance === null) {
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
return vnode
}
const instance = internalInstance.proxy
const bindings = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers] = directives[i]
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir
}
}
bindings.push({
dir,
instance,
value,
oldValue,
arg,
modifiers
})
}
return vnode
}
通过上面代码可知,withDirectives API的思路其实很简单,就是通过遍历指令配置对象,将配置的指令push至binding集合中。
总结
h函数其实是createVNode的语法糖,返回的就是一个Js普通对象。在createVNode API 在创建Vnode的时候,会对Vnode的props、children、ref、class、style等属性进行规范梳理或者合并。如果Type直接就是Vnode类型,则会返回深度克隆的Vnode对象。相较于HTML模板语法,使用h函数创建组件Vnode,更加灵活,也更抽象。
参考: