本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 7 篇,关注专栏
前言
上篇 runtime
文中我们了解到,虚拟 DOM
是 Vue 在运行时,通过 h
函数获取到 VNode
对象,本篇我们就来看下 h
函数是如何实现的。
案例
首先引入 h
函数,之后通过 h
函数生成一个 vnode
对象,并将其打印。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h } = Vue
const vnode = h(
'div',
{
class: 'test'
},
'hello render'
)
console.log(vnode)
</script>
</body>
</html>
h 实现
h
函数定义在 packages/runtime-core/src/h.ts
文件下:
ts
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
const l = arguments.length // 参数长度
// 参数为 2个
if (l === 2) {
// propsOrChildren 是否为对象 且不为数组
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
// propsOrChildren 是否为 vnode
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
}
} else {
// 参数超过 3个
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
// 参数为3个, children是否是 vnode
children = [children]
}
// 当前案例 直接走该逻辑
return createVNode(type, propsOrChildren, children)
}
}
可以看出 h
函数接收三个参数,当前 type
为 div
,propsOrChildren
为 { class: 'test'}
,children
为 hello render
。之后根据参数的长度不同走不同的判断逻辑,其核心是执行 createVNode
方法,实际执行的是 _createVNode
,该方法在 packages/runtime-core/src/vnode.ts
文件中:
ts
export const createVNode = (
__DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _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,
isBlockNode = false
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 是否是 vnode 通过 __v_isVNode 来判断
if (isVNode(type)) {
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// class & style normalization.
// class 和 style 的增强
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(props)! // 解析 props
let { class: klass, style } = props // 结构 class 赋值给 klass, style
if (klass && !isString(klass)) {
props.class = normalizeClass(klass) // 增强 class
}
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) // 根据 type 类型进行 shapeFlag 赋值 当前为 div 则 ShapeFlags.ELEMENT
? 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
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
这里 isVNode(type)
通过判断 type
是否是 VNode
,我们来看下 isVNode
方法:
ts
export function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}
主要通过 __v_isVNode
属性来判断是否是 VNode
,之后再判断 props
即传入的 { class: 'test'}
,对 class
、style
增强,这块我们放到之后来讨论。接着又对 shapeFlag
赋值,当前 type
为 div
string 类型,此时被赋值为 ShapeFlags.ELEMENT
即等于 1
,最后将处理好的 type
、props
、children
、shapeFlag
等参数传入 createBaseVNode
方法中。
_createVNode
方法核心一是对 class
、style
增强,二是对 shapeFlag
标记赋值。接着我们再看下 createBaseVNode
方法:
ts
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
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
} as VNode
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children) // 创建子节点
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// 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.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// 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.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
该方法首先定义了一个 vnode
对象,属性 __v_isVNode
标记为该对象是否为 VNode
对象。由于当前 needFullChildrenNormalization
默认传入的是 true
,所以直接执行 normalizeChildren(vnode, children)
方法来创建子节点,我们再来看下 normalizeChildren
方法:
ts
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode // 当前shapeFlag 是 1 children是字符串
// children 为 undefined 或 null
if (children == null) {
children = null
} else if (isArray(children)) { // 是否是数组
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') { // 是否是对象
if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) {
// Normalize slot to plain children for plain element and Teleport
const slot = (children as any).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 as RawSlots)._
if (!slotFlag && !(InternalObjectKey in children!)) {
// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
// a child component receives forwarded slots from the parent.
// its slot type is determined by its parent's slot type.
if (
(currentRenderingInstance.slots as RawSlots)._ === SlotFlags.STABLE
) {
;(children as RawSlots)._ = SlotFlags.STABLE
} else {
;(children as RawSlots)._ = SlotFlags.DYNAMIC
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
}
}
} else if (isFunction(children)) { // 是否是 函数
children = { default: children, _ctx: currentRenderingInstance }
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children) // 此时 'hello render'
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children as string)]
} else {
type = ShapeFlags.TEXT_CHILDREN
}
}
vnode.children = children as VNodeNormalizedChildren
// 9 按位或赋值 vnode.shapeFlag |= type 等同于 vnode.shapeFlag = vnode.shapeFlag | type
vnode.shapeFlag |= type
}
该方法接收两个参数,一个是定义的 vnode
对象,一个是 children
即 hello render
。之后再从 vnode
对象中解构出 shapeFlag
即当前 string
类型为 1,之后根据 children
类型不同对children
、type
和 shapeFlag
重新赋值。由于当前 children
为 string 类型,执行 children = String(children)
,此时children
为 hello render
,并将其 vnode.children = children
重新赋值。type = ShapeFlags.TEXT_CHILDREN
即 type
为 8,最后对 vnode.shapeFlag |= type
按或位赋值即等于 9
。
这里拓展下 |=
按或位赋值,vnode.shapeFlag |= type
等同于 vnode.shapeFlag = vnode.shapeFlag | type
。当前 vnode.shapeFlag = 8
,type = 1
,转为二进制:
ts
// type = 1
00000000 00000000 00000000 00000001
// shapeFlag = 8
00000000 00000000 00000000 00001000
// 或 就是通过上下 ↕ 比较,如果上下是 0 则是 0,上下是 0 和 1 则是 1
// 结果是 9
00000000 00000000 00000000 00001001
所以此时计算后的 vnode.shapeFlag = 9
,之后 createBaseVNode
执行完毕返回 vnode
对象,至此 h
函数执行完毕,打印 vnode
对象:
我们再回过来看下 h
函数如何对 class style
增强的,该逻辑在 _createVNode
方法中:
ts
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// 省略
// class & style normalization.
// class 和 style 的增强
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(props)! // 解析 props
let { class: klass, style } = props // 结构 class 赋值给 klass, style
if (klass && !isString(klass)) {
props.class = normalizeClass(klass) // 增强 class
}
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)
}
}
// 省略
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
结合案例:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render } = Vue
// <div :class="{ red: true }">增强的 class</div>
const vnode = h(
'div',
{
class: {
red: true
}
},
'增强的 class'
)
render(vnode, document.querySelector('#app'))
</script>
</body>
</html>
可以看出 { class: klass, style } = props
对 props
解构,并将 class
赋值给 klass
,如果存在 klass
且不为 string类型,则执行 props.class = normalizeClass(klass)
,对其 props.class
重新赋值。我们再看下 normalizeClass
方法:
ts
export function normalizeClass(value: unknown): string {
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)) {
// 是对象 则 for in 再拼接返回
for (const name in value) {
if (value[name]) {
res += name + ' '
}
}
}
return res.trim()
}
该逻辑也较容易理解,根据 value
类型,如果是字符串则直接返回;如果是数组则递归迭代再拼接返回;如果是对象则迭代再拼接返回。由于当前 value
是对象 { red: true }
,所以此时的 res
为 red
:
最终结果:
由于 style
同 class
逻辑类似,这里就不再具体展开讲解。
另外还有几种特殊的场景,比如 h
函数接收的是一个组件:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render } = Vue
const component = {
render() {
const vnode1 = h('div', '这是一个 component')
console.log(vnode1)
return vnode1
}
}
const vnode2 = h(component)
console.log(vnode2)
render(vnode2, document.querySelector('#app'))
</script>
</body>
</html>
输出:
可以看出 vnode1
和 vnode2
仅仅只是根据类型不同对 shapeFlag
赋值不同。再比如 children
参数为一个数组:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h } = Vue
// 先执行了 children h 函数 p1
// shapeFlag为 17 代表是 element + array children 为 9 代表是 element + text children
const vnode = h(
'div',
{
class: 'test'
},
[h('p', 'p1'), h('p', 'p2'), h('p', 'p3')]
)
console.log(vnode)
</script>
</body>
</html>
这里需要注意的是会优先执行 children
的 h
函数,其结果也是标记 shapeFlag
不同值:
最后 Vue
中还声明了 Text
、Comment
、Fragment
三种类型,其值为 Symbol(Text)
、Symbol(Comment)
、Symbol(Fragment)
:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render, Text, Comment, Fragment } = Vue
const vnodeText = h(Text, '这是一个 Text')
console.log(vnodeText)
render(vnodeText, document.querySelector('#app'))
const vnodeComment = h(Comment, '这是一个 Comment')
console.log(vnodeComment)
render(vnodeComment, document.querySelector('#app'))
const vnodeFragment = h(Fragment, '这是一个 Fragment')
console.log(vnodeFragment)
render(vnodeFragment, document.querySelector('#app'))
</script>
</body>
</html>
结果:
可以看出仅仅只是 type
类型不同。
总结
createVNode
核心是处理shapeFlag
赋值,之后在createBaseVNode
中又通过shapeFlag
和type
根据按位或运算,重新对shapeFlag
赋值。h
函数本质上是对四个属性处理children
、props
、shapeFlag
、type
和class style
的增强。