Vue
简单易上手,只需要简单的文档说明就能上手开发。虽然本人也有一直使用Vue 2.0
的项目开发经验,以前也只了解一点核心代码逻辑,没有全面阅读过Vue 2.0
的源码。Vue 3.0
发布后我也有了一些Vue 3.0
项目使用经验,顺藤摸瓜来学习下Vue 3.0
源码,向高手学习下编码技巧,以便在项目中更加游刃有余。
由于Vue 3.0
使用了TypeScript
进行了重构,所以阅读本系列前需要对TypeScript
基础语法有所了解,此外需要了解递归调用和函数柯里化等。
Vue 3.0
系列文章预估会有20个左右的文章篇幅,每篇文章只会围绕所涉及知识点学习,这样分析起来会比较清晰,否则绕在了一起会很混乱。
如果不想看复杂的分析过程,可以直接看最后的图片总结。
我们经常会使用如下的代码:
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
App就是一个Vue组件,浏览器无法识别这个组件,也不知道如何渲染这组件,那Vue
是如何将App组件渲染成真实的DOM呢?本文我们来学习下Vue
组件转换成真实DOM
的渲染过程。
应用程序初始化
createApp
入口函数
export const createApp = ((...args) => {
// 1.
const app = ensureRenderer().createApp(...args)
// 2.
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 省略...
}
return app
}) as CreateAppFunction
createApp
入口函数主要做了两件事情:
- 使用
ensureRenderer().createApp()
创建app
对象 - 重写
app
的mount
方法。
创建app
对象
创建渲染器对象
渲染器是具有平台渲染核心逻辑的JS对象。
Vue
可以做为跨平台渲染,所以不一定是DOM的渲染。
用ensureRenderer()
创建一个渲染器对象:
// 渲染器对象
let renderer: Renderer | HydrationRenderer
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer(rendererOptions))
)
}
这里先判断有没有渲染器,没有再去创建一个渲染器,属于一个延时创建的方式,只有需要的时候才会去创建渲染器。
渲染器初始化的时候会传入一个渲染配置参数---rendererOptions
,它定义了attribute
处理方法, DOM
操作方法等。
export interface RendererOptions<
HostNode = RendererNode,
HostElement = RendererElement
> {
// 处理Prop,attributes等
patchProp(
el: HostElement,
key: string,
prevValue: any,
nextValue: any,
isSVG?: boolean,
prevChildren?: VNode[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn
): void
// 插入节点
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
// 省略...
}
虽然开发者不需要直接操作DOM,但是可以猜测到所有的组件会被转换成DOM。渲染器的这个配置参数包含直接操作DOM的方法,因此是非常关键的一个配置。
createRenderer
方法内部直接调用baseCreateRenderer
方法:
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions) {
return baseCreateRenderer(options)
}
baseCreateRenderer
方法的代码如下:
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 1.
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// 2.
const patch: PatchFn = (
n1,
n2,
...
) => {}
const processComponent = (
n1: VNode | null,
n2: VNode,
...
) => {}
// 省略众多方法...
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
// 3.
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
- 首先解构传入的RendererOptions对象
options
, 然后修改了操作DOM的方法的参数名;- 定义了众多的渲染相关的方法,其中最重要的是
render
方法。render
调用了一个重要的patch
方法,patch
方法又会调用其他的方法,譬如组件处理相关的processComponent
方法。如果某个方法需要操作DOM,那就会调用RendererOptions对象options
中的方法。- 最后返回一个包含
render
和createApp
方法的对象。hydrate
为undefined
。
上面的createApp
的值是createAppAPI
的函数返回值,那它又是做什么的呢?
export function createAppAPI(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction {
// 1.
return function createApp(rootComponent, rootProps = null) {
// 2.
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 2.
const context = createAppContext()
// 3.
const installedPlugins = new Set()
// 4.
let isMounted = false
// 5.
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
},
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
}
return app
},
component(name: string, component?: Component): any {
if (!component) {
return context.components[name]
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (!directive) {
return context.directives[name] as any
}
context.directives[name] = directive
return app
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
if (isHydrate && hydrate) {
hydrate(vnode as VNode, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
},
unmount() {
if (isMounted) {
render(null, app._container)
delete app._container.__vue_app__
}
},
provide(key, value) {
context.provides[key as string] = value
return app
}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
createAppAPI
的执行结果是createApp
方法,方法的执行结果是返回一个App
对象;
- 注意:千万不要混淆了
Vue
框架中的App
和开发者定义的App
,开发者定义的App
其实是函数的传入参数rootComponent
这个根组件,rootProps
是开发者传入的根组件相关的props
。
- 检查
props
的合法性---如果不为null,必须是对象;- 创建一个AppContext对象
context
, 它包含一个app
属性指向App
对象,plugin,provide,directive和component等都是挂载在此对象上;installedPlugins
用来存储安装的Plugin;isMounted
置为false,标记为未挂载;- 生成了一个
app
对象,它包含一些属性:_component
为开发者定义的App
根组件,_props
为开发者传入的根组件相关的props
,_context为上面定义的AppContext对象context
。它还包含了一些方法,use
安装插件方法,mixin
混入方法,component
全局定义组件方法,directive
指令方法,mount
挂载方法,unmount
卸载方法,provide
共享数据方法。
- 这里我们可以看到
Vue 3.0
一个重大的变化就是这些方法从以前Vue 2.0
的全局方法变成了app对象方法。- 这里面一个重要的方法是
mount
挂载方法,具体功能后面会做介绍。这个方法持有了render
渲染方法,所以调用mount
方法的时候不需要传递渲染器,这是函数柯里化的一个重要技巧。
重写mount
方法
我们回到createApp
入口函数,上个小节分析了const app = ensureRenderer().createApp(...args)
这行代码的实现细节,我们接下来分析接下来的流程:
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// 1.
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 2.
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 3.
const component = app._component
// 4.
container.innerHTML = ''
// 5.
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
// 6.
container.removeAttribute('v-cloak')
// 7.
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction
- 解构了
app
中的mount
方法,然后重写app
中的mount
方法;- 标准化容器,如果容器是一个字符串,则会调用
document.querySelector(container)
找到对应的DOM节点,这就是我们可以传入"#app"作为容器的原因;- 将
app._component
赋值给component
对象,这个对象其实就是开发者提供的App根组件;- 清除容器的内容,即如果容器有子节点将会被清除掉;
- 调用框架中的App的的
mount
方法,即createAppAPI
方法中的app对象的mount
,进行挂载,这个流程下面详细介绍;
- 看一眼
mount
方法调用:mount(container, true, container instanceof SVGElement)
,先了解下第一个是容器,第二个参数是true,第三个参数是false。
- 清除掉容器的v-cloak Attribute,这个可以属性可以和
{display:none}
结合解决网络慢的情况下的页面闪动问题;- 容器加上data-v-app Attribute,这个属性没啥实际作用,就只是个标记;
我们来到App
的mount
方法:
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 1.
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 2.
vnode.appContext = context
// 3.
render(vnode, rootContainer, isSVG)
// 4.
isMounted = true
// 5.
app._container = rootContainer
return vnode.component!.proxy
}
}
- 首先根据
rootComponent
和rootProps
创建对应的VNode对象vnode
;- 给
vnode
的appContext
赋值为创建app时初始化的context
, 这个context
上面介绍过,可以挂在插件等内容,此外也有app属性指向app;- 渲染
vnode
,这个下面会重点介绍,暂不深入;- 标记为已挂载;
- 给
app
的_container
赋值为父容器;
App
的mount
流程中有两个重要的逻辑:创建VNode
的createVNode
和渲染VNode
的render(vnode, rootContainer, isSVG)
,接下来我们就来介绍他们。
创建VNode
VNode
是在前端开发描述DOM的JS对象,它可以描述不同类型的节点,可以是组件节点,也可以是普通的元素节点,还有其他多种类型的节点。DOM是树状结构,VNode
也是树状结构。
VNode
和Flutter中的Widget
类似,只是节点信息的描述树。Flutter中真正的渲染树是RenderObject树,而Vue
的渲染树在前端开发中则是DOM树。Flutter跨平台的逻辑是渲染逻辑根据不同的平台有差异,
Vue
基于VNode
也是可以跨平台的,譬如Weex和uniapp就是利用Vue
实现多平台的开发。
createVNode
createVNode
内部指向的是_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 {
// 1.
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
if (props) {
// 2.
props = guardReactiveProps(props)!
// 3.
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
// 4.
if (isObject(style)) {
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 5.
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
// 6.
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
- 如果传入的
type
参数本来就是VNode
,那么就复制一份并且标准化子节点并返回;- 如果有
props
参数就进行参数标准化,如果是响应式对象就拷贝一份,否则不做处理;响应式的对象复制是为了避免修改响应式的数据造成其他副作用;- 对class进行标准化,如果是字符串就直接返回对应的原值,如果是数组就对数组的每个元素进行标准化,如果是对象就获取对象的属性值为true然后用空格分开;参考文档
- 对style进行标准化,如果是字符串或者对象直接返回原值,如果是数据就对数组中的每个元素的key和value组合成style对象;参考文档;
- 根据
type
的类型编码成ShapeFlags,如果传入的是Object
,则编码成ShapeFlags.STATEFUL_COMPONENT;- 最后调用
createBaseVNode
进行真正的创建VNode
;
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
) {
// 1.
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) {
// 2.
normalizeChildren(vnode, children)
} else if (children) {
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
return vnode
}
- 生成
vnode
对象,包含了type
和props
参数;- 标准化子节点---将子节点赋值给
vnode
对象的children
属性,根据子节点的ShapeFlags
修改点前VNode
的ShapeFlags
;
渲染VNode
我们接下来看看mount
方法中的render(vnode, rootContainer, isSVG)
的逻辑:
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 1.
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 2.
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// 3.
container._vnode = vnode
}
- 如果
vnode
为null
,则卸载父容器的_vnode
对象;- 如果
vnode
不为null
,则调用patch
方法,第一次container._vnode
为null
,vnode
为开发者的App生成的VNode
,container
为#app
DOM元素;- 父容器的
_vnode
设置为vnode
;
挂载和更新VNode
// 1.
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 2.
if (n1 === n2) {
return
}
// 3.
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 4.
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) {
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) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
patch
方法的第一个参数是旧的VNode
,第二个参数是新的VNode
,第三个参数是父节点DOM元素;- 如果新旧
VNode
是同一个对象就不需要操作,直接返回;- 如果新旧
VNode
的VNodeType
不一致,就先卸载旧的VNode
,将旧的VNode
置空,再挂载新的VNode
;- 根据新
VNode
的type
和shapeFlag
,如果是组件则进入processComponent
方法,如果普通节点就进入processElement
方法;
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
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)
}
}
如果旧的VNode
为null
,且不是keep-alive
组件,则调用mountComponent
方法进行组件的挂载。
mountComponent
挂载组件
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 1.
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 2.
if (!(__COMPAT__ && compatMountInstance)) {
setupComponent(instance)
}
// 3.
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
- 创建组件实例ComponentInternalInstance对象
compatMountInstance
,我们看到创建的实例接收了vnode
和父容器节点的DOM
元素, 其他的属性初始化的时候都是默认的;- 设置组件实例主要是从持有的
vnode
中获取props
和slot
等属性然后进行相应的属性设置,此外开发者如果再组件中有使用setup
方法,那这个setup
方法也会被调用,持有其中的属性和方法;- 根据组件实例
compatMountInstance
,vnode
和父容器节点的DOM
元素创建一个带副作用渲染函数;
setupRenderEffect
- 创建带副作用的渲染函数
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 1.
const componentUpdateFn = () => {
if (!instance.isMounted) {
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
}
// 2.
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
)
// 3.
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
effect.allowRecurse = update.allowRecurse = true
// 4.
update()
}
- 创建了一个初次渲染或者更新渲染的
componentUpdateFn
方法,其实他们都会调用patch
方法,他们的区别是初次渲染的第一个参数是null
,而更新渲染时第一个参数是旧的VNode
;
componentUpdateFn
中调用的patch
方法有一个特点就是传了instance
,即把ComponentInternalInstance对象当做参数传入patch
方法。
- 创建了一个ReactiveEffect对象
effect
,这个对象的第一个参数fn
是一个函数,当effect
调用update
方法时会执行fn
的函数调用;- 将
effect
的run
函数赋值给instance
的update
属性,并给update
标记一个id
;- 执行
update()
, 也就是执行componentUpdateFn
方法,componentUpdateFn
方法会调用patch
方法,递归下去。
patch
到最后肯定是处理普通元素的VNode
,所以接下来我们就了解下普通元素的VNode
是如何处理的。
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'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
}
}
processElement
在n1
为null
的时候是进入mountElement
挂载元素方法。
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
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 1.
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// 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',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
// 4.
hostInsert(el, container, anchor)
}
- 通过
hostCreateElement
创建DOM元素节点,我们前面介绍过hostCreateElement
是创建渲染器时候传入的配置参数,本质是调用doc.createElement(tag, is ? { is } : undefined)
;- 对子节点进行处理:如果子节点是文本,则最后实际调用
el.textContent = text
;如果子节点是数组,调用mountChildren
方法,数组的每个子节点调用patch
挂载子节点;- 判断如果有
props
,通过hostPatchProp
方法给这个DOM节点设置相关的class
,style
,event
等属性。- 将创建的DOM元素挂载到
container
上。
我们看到了先进行path
深度优先遍历,然后再挂载创建的DOM元素,也就是说挂载DOM元素是从树的子节点开始然后逐步挂载直到根节点,最后整体挂载到#app
这个元素上。至此整个DOM数渲染完成了。