vue3源码学习2-创建和渲染vnode
创建vnode
我们在第一节中在packages/runtime-core/src/apiCreateApp.ts文件的createAppAPI方法中,app.mount()时:
// 通过 createVNode 方法创建了根组件的vnode
const vnode = createVNode(rootComponent, rootProps)
packages/runtime-core/src/vnode.ts中查看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 (props) {
// 标准化一些class和style
}
// 对vnode类型编码
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
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
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
) {
// 标准化子节点,把不同数据类型的children转成数组或者文本类型
normalizeChildren(vnode, children)
return vnode
}
渲染vnode
还是在packages/runtime-core/src/apiCreateApp.ts文件的createAppAPI方法中,app.mount()时,我们使用了render(vnode, rootContainer, isSVG)来渲染vnode,这方法来自packages/runtime-core/src/renderer.ts文件的baseCreateRenderer方法中调用createAppAPI时传入,所以我们在renderer.ts文件的baseCreateRenderer方法中查看:
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
// 如果vnode是null, 销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 更新或创建组件
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 缓存组件, 表示已经渲染
container._vnode = vnode
}
接下来看patch的实现,patch是补丁的意思,可以根据vnode挂载DOM,也可以根据新旧vnode更新DOM
// n1表示旧的vnode,n1为null时,表示第一次挂载
// n2表示新的vnode,会根据这个vnode的类型执行不同的逻辑
// container表示容器,vnode渲染生成DOM后,会挂载到container下面
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 如果没有更新就返回
if (n1 === n2) {
return
}
// 存在新旧节点,而且类型不同,销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
processText(n1, n2, container, anchor)
break
case Comment:
// 处理注释节点
processCommentNode(n1, n2, container, anchor)
break
case Static:
// 处理静态节点
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
// 处理片段
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通DOM元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理TELEPORT...
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理SUSPENSE...
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
}
因为初始化的时候传进来的是APP组件,所以先看组件处理processComponent
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
// 如果n1为null。则执行挂载组件
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 否则执行更新组件
updateComponent(n1, n2, optimized)
}
}
继续看mountComponent方法
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例:通过对象的方式去创建了当前渲染的组件实例
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
创建和设置组件实例后面再看,先看运行带副作用的渲染函数setupRenderEffect方法
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用渲染函数
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 渲染组件生成子树vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树挂载到container
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// 保留渲染生成的子树的根节点
initialVNode.el = subTree.el
instance.isMounted = true
} else {
// 更新组件。。。
}
}
// 利用ReactiveEffect创建的副作用渲染函数,副作用的意思就是当instance数据变化时,
// ReactiveEffect包裹的componentUpdateFn方法会重新执行一遍,即重新渲染组件
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
}
patch中的组件处理先分析到这里,我们接着看处理普通DOM元素 processElement
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 如果n1是null,挂载元素节点
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 否则更新元素节点
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
接着看挂载元素节点方法mountElement
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 创建DOM元素节点
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 处理子节点是纯文本的情况
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 处理子节点是数组的情况
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (props) {
// 处理props: class、style、event等属性
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 把创建的DOM元素节点挂载到container上
hostInsert(el, container, anchor)
}
hostCreateElement创建DOM元素,与平台相关,在WEB环境下定义:(packages/runtime-dom/src/nodeOps.ts)
// 内部实现是调用底层DOM API: document.createElement 创建元素
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
子节点的处理:如果是纯文本,在WEB环境下定义:(packages/runtime-dom/src/nodeOps.ts)
setElementText: (el, text) => {
el.textContent = text
},
子节点的处理:如果是数组
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
// 预处理child
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
// 递归patch挂载child
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
最后使用hostInsert把创建的DOM挂载到container,这个方法也是平台相关(packages/runtime-dom/src/nodeOps.ts)
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},