「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」
上一小节文章,讲述了 createApp
创建应用实例主流程逻辑,这篇就跟着上章文章继续看看虚拟节点的创建、渲染以及挂载。注意,这篇主要讲述新组件挂载流程,关于组件更新相关内容下篇再继续。
附上应用上下文执行mount的源码:PS:文章目录顺序及执行流程。
// ... 其他代码省略
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
// 根据根组件创建虚拟节点
const vnode = createVNode(rootComponent as Component, rootProps)
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer)
}
}
// 渲染虚拟节点
render(vnode, rootContainer)
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
},
复制代码
如果你对上一篇内容不熟悉,建议先熟悉下流程,内容文章如下:
createVNode
这个方法是根据组件和组件属性,生成一个VNode虚拟节点。
虚拟节点是什么,有什么好处呢?
VNode的本质是一个描述DOM的JavaScript对象,是对抽象事物的描述。
- 跨平台
- 为数据驱动视图提供了媒介
- 对于频繁通过JavaScript操作DOM的场景,VNode性能更优,因为它会等收集到足够的改变时,再将这些变化一次性应用到真实的DOM上。
下面看下生成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) {
// 不传,默认Comment类型的虚拟节点
type = Comment
}
// 已经是虚拟节点,则克隆一个,返回
if (isVNode(type)) {
const cloned = cloneVNode(type, props)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// 类组件标准化。
if (isFunction(type) && '__vccOpts' in type) {
type = type.__vccOpts
}
// style和class标准化。
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, 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)
}
}
// 将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
// 创建虚拟节点
const vnode: VNode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
el: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
... // 其他属性省略
}
// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children)
...
return vnode
}
复制代码
从代码解析我们可以看出,createVNode做了如下几件事:
- 对属性props标准化
- 将VNode类型信息进行编码为位图
- 创建VNode对象
- 对子节点进行标准化
render
存在VNode了,下面看看如果进行渲染。
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
// 虚拟节点为null,则销毁容器内的虚拟节点
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建、或者更新节点,创建的时候这里container._vnode是不存在的
// 第一个参数: 旧的虚拟节点
// 第二个参数:新的vnode
// 第三个参数:vnode转化为dom,最终要挂载的dom容器
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
复制代码
render函数很简单,根据传参决定是否销毁、还是创建或者更新组件。下面看看创建具体流程。
patch
包含了组件创建、和更新的相关实现,我们这里先看看创建相关逻辑。
const patch: PatchFn = (
n1, // 旧节点
n2, // 新节点
container, // DOM容器,vNode渲染成dom会挂载到该节点下
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// 存在旧节点,且新旧节点不同,则卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// PatchFlags.BAIL:一个特殊标志,表示differ算法应该退出优化模式
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理Fragment元素
break
default:
// 处理DOM元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件元素
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理TELEPORT
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理SUSPENSE
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
复制代码
从源码可以看出,当新旧虚拟节点不同,会先卸载旧节点。且vNode存在八种不同的类型,在patch函数中,会根据vNode的类型去做对应的处理,挂载DOM,或者更新DOM。
下面我们看下具备代表性的如何处理组件和DOM元素。
processElement
看看如何挂载DOM元素的。
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 旧节点不存在的话,则挂载新元素,否则更新
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
复制代码
本文只看挂载元素的过程,下面看看 mountElement
方法,因为函数代码多,下面删减下,只关注主流程。
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const {
type,
props,
shapeFlag,
transition,
scopeId,
patchFlag,
dirs
} = vnode
// 创建dom元素节点
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// vNode.el非空,表示它要被重用,只有静态vNode可以被重用,这里采用克隆。
// 只在生产中生效,克隆的树不能被HMR更新
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 1. 创建新元素
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
// 2. 先挂载子节点,因为节点可以依赖于已经呈现的字节点内容
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',
optimized || !!vnode.dynamicChildren
)
}
// 当前元素el处理属性相关,如style/class/event等
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 处理节点挂载前的钩子函数
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// ... 其余元素特性处理,如,slot/transition
}
// 把元素挂载到容器上
hostInsert(el, container, anchor)
}
复制代码
整个过程我们可以缕清如下:
- 创建DOM元素,如果
vNode.el
非空且为静态虚拟节点,则直接克隆一个。 - 先挂载元素子节点,因为当前节点可能依赖子节点的属性。如果子节点是文本节点,则直接设置节点内容;如果节点是数组,则遍历子节点,递归执行patch操作。相关代码,可自行查看。
- 属性存在,则处理元素的相关属性。
- 挂载元素到容器
container
上。
processComponent
这里直接看组件挂载的逻辑。
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 1. 创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 将keepAlive注入渲染器内部
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 设置组件实例
setupComponent(instance)
// 设置并执行带副作用的渲染函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
复制代码
可以看出该函数,执行了一下步骤:
- 创建组件实例
- 设置组件实例
- 执行带副作用的渲染函数
下面看看,如果带副作用的渲染函数是如何执行的。
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, a, parent } = instance
// 组件实例生成子树vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
if (el && hydrateNode) {
// 省略
} else {
// 把子树挂载到container上
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// 保存渲染生成的子树根节点
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) {
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, initialVNode)
}, parentSuspense)
}
//为keep-alive节点激活钩子.
if (
a &&
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
) {
queuePostRenderEffect(a, parentSuspense)
}
instance.isMounted = true
} else {
// 更新组件相关逻辑
}
}, prodEffectOptions)
}
复制代码
从源码可以处理如下:
- effect创建了一个副作用渲染函数
componentEffect
,当组件数据变化时,该函数会重新执行一次。 - 渲染组件生成子树
subTree
,并把子树挂载到 - 将子树的根节点保存到当前节点
- 整个组件挂载过程,执行了一些钩子函数,如
beforeMount、Mount
,以及keep-alive
的处理。
总结
至此便阅读了组件挂载过程。首先会创建虚拟节点VNode,然后执行渲染逻辑。若创建的VNode为null,则组件执行卸载过程,否则执行创建或者更新流程。本篇文章讲解挂载过程,创建的VNode存在8种类型,
我们针对组件和元素进行了分析。挂载元素的流程是:创建DOM元素->更新元素属性->递归挂载子节点,这里DOM相关操作,可以参考 nodeOps.ts
文件,底子里也是通过dom api来完成。挂载组件的过程是,创建组件实例->设置组件实例->执行带副作用的渲染函数,渲染组件子树,关于组件渲染更细则实现可以阅读 componentRenderUtils.ts
文件。
相关代码如下:
packages/runtime-dom/src/index.ts
packages/runtime-core/src/vnode.ts // 虚拟节点
packages/runtime-core/src/renderer.ts // 渲染器
packages/runtime-core/src/componentRenderUtils.ts // 组件渲染方法
packages/runtime-dom/src/nodeOps.ts // 描述节点的操作