前言#
我们通过createApp了解到了render function到真实dom这个过程。
坏蛋Dan:vue runtime源码分析学习——day3:确定后续分析流程
坏蛋Dan:vue runtime源码分析学习——day4:createApp
不过有几个点没有分析:
hmrcreateVNodepatch
hmr其实我们稍微的接触到了一下,但是还没接触到核心,后面我们遇到了再说。
今天我们来看下createVnode的代码里具体做了什么。
createVnode#
选择测试用例#
在分析之前,我们来找下测试用例
本来打算自己写一个的,但是模拟对应的runtime环境不是一件简单的事情,尤其是几个全局变量,其中还涉及到webpack对于chunck的分割,这又是一个难点。如果不涉及webpack相关的功能,其实还是有可能模拟的。
所以这里依旧是退而求其次,直接用测试用例里的,这些用例自然都是没经过编译的代码。
(我开始有些后悔了,如果直接基于浏览器调试,也就是调试build之后的dist代码,那么就会很舒服,但是可读性小了很多,如果你不喜欢这种调试方式,可以直接基于浏览器调试,也就没这么麻烦了,另外由于同个包中所有代码都在同一个文件中,所以不用各个文件翻阅也是一件不错的事情)
这里我们不使用昨天我们在vue/index.spec.ts中的测试用例了。
我们进入到createVNode函数所在的包中runtime-core中找,这里面有个vnode.spec.ts,专业对口。

就这货了,看着有缘。
话不多说,开始调试。
openBlock#
openBlock我们在编译阶段也经常遇到,由于将一块node-tree标记为block,这样方便跟踪,比如slot等就是开block的。
export const blockStack: (VNode[] | null)[] = []
export let currentBlock: VNode[] | null = null
/**
* Open a block.
* This must be called before `createBlock`. It cannot be part of `createBlock`
* because the children of the block are evaluated before `createBlock` itself
* is called. The generated code typically looks like this:
*
* ```js
* function render() {
* return (openBlock(),createBlock('div', null, [...]))
* }
* ```
* disableTracking is true when creating a v-for fragment block, since a v-for
* fragment always diffs its children.
*
* @private
*/
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
} 也是栈结构,因为node-tree也是嵌套的结构,那么block标记自然也是嵌套的结构,所以用栈来保证嵌套层级的正确和顺序的正确再适合不过了。
_createVNode#
在分析之前,我们要记住,vnode是针对于每个节点的(组件自身也有vnode),不然看到后面可能就有些混了。
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
}
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.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
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)
}
}
// 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
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}这个createVNodeWithArgsTransform方法就不看代码了,它是用来搭配test-utils的,我之前关于单元测试相关的文章中也有用到,感兴趣的大佬可以去看下。
如果这个传入的节点不对,比如是个symbol或者undefined,那么它将被当作是一个注释节点。
如果它自身就是一个vnode,比如``,它自身就是vnode,那么这个时候就直接copy它,然后加上一些内置的东西。
如果此时允许blockTree并且当前节点不是block节点而是block节点里的子节点,那么这个时候它就会被放到currentBlock里。
然后将这个cloneNode的flag赋值为BAIL
BAIL我们来看下描述:
/**
* A special flag that indicates that the diffing algorithm should bail out
* of optimized mode. For example, on block fragments created by renderSlot()
* when encountering non-compiler generated slots (i.e. manually written
* render functions, which should always be fully diffed)
* OR manually cloneVNodes
*/
BAIL = -2一个特殊标志,指示dff算法应退出优化模式。例如,在 renderSlot() 创建的block片段上,当遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全diff)时 或手动克隆VNodes
简单的说就是遇到这种直接乱棍打死,而不是根据flag等方式跳过diff。
然后返回这个cloneVNode。
如果不是vnode,那么继续判断它是否是一个class component
如果是一个class component,那么它会变成它的__vccOpts,官方给的class component我试了下发现并没有效果,看了下对应的测试用例张这样:

不纠结,我们接着往下看。
2.x兼容的一如既往的就跳过了
然后处理dom的props也就是属性
guardReactiveProps:
export function guardReactiveProps(props: (Data & VNodeProps) | null) {
if (!props) return null
return isProxy(props) || InternalObjectKey in props
? extend({}, props)
: props
}
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
export function isReactive(value: unknown): boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
export const InternalObjectKey = `__vInternal`这里对于响应式的属性或者被proxy代理的对象都需要clone一份出来,为了可修改。
normalizeClass:这个方法我们在编译阶段其实有说过,_normalizeClass辅助函数,简单的说就是把所有的class做拼接。normalizeStyle:同理,这个方法之前编译阶段有说过,这里也不多说了,和class一样是在做拼接。注意则合理都覆盖了原props的属性。
你可能会有疑惑,为啥这里可以直接拼接,不是有动态的属性吗?因为此时是runtime阶段,部分已经经过之前的辅助函数处理(script部分的执行早就开始了,此时的数据应该已经暂时固定下来了),都已经确定下来了。
然后判断这个vnode的类型,可能就是一个节点,也可能是一个组件,比如我们的sfc组件在runtime的样子,也有可能是内部组件,比如Teleport或者Suspense。
另外认识下ShapeFlags
export const enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}最后调用createBaseVNode
createBaseVNode/createElementVNode#
是的,createBaseVNode也就是我们编译阶段遇到的createElementVNode这个辅助函数。

所以这里其实不是每个vnode都是通过createVNode进入的,比如原生元素就直接调用的createBaseVNode生成vnode,而App这样的组件就是通过createVNode生成的,因为它自身不是使用的createElementVNode包裹的

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。
但是这里面有几个东西是有必要了解的。
normalizeChildren:简单的说就是用来处理这个vnode的children node的,但是一般是不需要处理的, 代码分析放后面。SuspenseImpl.normalize(vnode):这个代码分析也放到后面,简单的说就是包裹它的子dom,等到可以渲染的时候再render。
如果不需要处理子vnode,那就是说明子节点都是确定的了,要么就是文本节点,要么就是数组节点。
一般也不需要处理,因为编译阶段我们已经处理完毕了。
接着就是回收这个vnode,毕竟是在block里的。当然,只有需要patch的才会被放到block里面(组件除外,即使组件不需要更新,但是它依旧需要被patch,毕竟你不能让一个组件突然就没得了,得先保留它的实例,不然到时候通过unmount的方式卸载时找不到)。
最后返回这个vnode。
normalizeChildren#
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
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)
// 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
vnode.shapeFlag |= type
}这个方法简单的说就是用来处理一些子节点类型的,大部分在编译阶段就已经被处理好了,但也不是所有。
那么什么情况会进入到这个方法中呢?
任何通过createVNode的方式创建vnode的节点,比如文本节点调用的辅助函数是createTextVNode,又比如注释节点使用的createCommentVNode,它们都是调用的createVNode。
而createVNode传给createBaseVNode的needFullChildrenNormalization是true,所以就会调用这里的normalizeChildren。另外,我们的slot和teleport代码也都会进入这里,它们需要确定上下文,需要使用_withCtx包裹。
总结#
今天写的有些水。。。这俩迭代有个需求很搞心态。。
这里有一点注意的,那就是编译阶段最后render function里的一个个node和这个vnode的关系。
vnode是render function里的vnodeCall执行后的产物。以_createTextVNode("1111")为例子,它并不是一个vnode,它是一个vnodeCall,只有它被执行之后它才变成了vnode。而这个_createTextVNode内部就是基于我们今天的主角createVNode的。
编辑于 2023-02-22 10:21・IP 属地广东
