先看一下vue-next官方文档的介绍:
每个 Vue 应用都是通过用
createApp
函数创建一个新的应用实例开始的
传递给
createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到
,我们应该传递
#app
我们将分为两部分进行渲染过程的理解:
- 创建应用实例,函数
createApp
的剖析 - 应用实例挂载, 函数
mount
方法挂载过程
本篇详细讲述调用应用实例方法mount
过程
下面是一个简单的demo
双向绑定:{{value}}
const { createApp } = Vue
const helloComp = {
name: 'hello-comp',
props: {
personName: {
type: String,
default: 'wangcong'
}
},
template: 'hello {{personName}}!
'
}
const app = {
data() {
return {
value: '',
info: {
name: 'tom',
age: 18
}
}
},
components: {
'hello-comp': helloComp
},
mounted() {
console.log(this.value, this.info)
},
}
createApp(app).mount('#app')
现在我们从mount函数为入口,去了解应用挂载的过程。
挂载 mount
现在回顾一下在demo中我们调用的方法createApp(app).mount('#app')
此时调用的这个mount
方法是在runtime-dom
模块重写之后的,在内部依然会执行应用实例的app.mount
方法。
重写mount
方法,在app._component
引用的根组件对象上面添加了template
属性,用来获取html
中的模板字符串。
并返回了proxy
,也就是根组件实例vm
。
// runtime-dom模块重写了`app.mount`方法
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
// app._component引用的对象是根组件对象,就是我们传入createApp方法的根组件对象
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
proxy
是调用了mount
方法返回的:const proxy = mount(container)
现在看一下在应用实例app中定义的mount
方法,它做了两件事情:
- 执行
createVNode
方法生产VNode
- 执行
render
方法。(上一篇文中所提:父级作用域函数createAppAPI(render, hydrate)
接受的第一个参数)
mount(rootContainer: HostElement, isHydrate?: 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
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode, rootContainer as any)
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version)
}
return vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
}
createVNode
先看一下VNode包含的类型
export type VNodeTypes =
| string
| VNode
| Component
| typeof Text // 文本
| typeof Static // 静态组件 纯html
| typeof Comment
| typeof Fragment // 多根组件
| typeof TeleportImpl // 内置组件传送
| typeof SuspenseImpl // 内置组件悬念 一般配合异步组件
createVNode
函数内部实际会调用_createVNode
函数
export const createVNode = (__DEV__
? createVNodeWithArgsTransform
: _createVNode) as typeof _createVNode
我们传入createApp
方法的根组件对象会作为_createVNode
方法接收的第一个参数type
,并在执行后会返回一个VNode
。VNode中包含一个shapeFlag
标识类型,在后面patch
过程中进入不同的处理逻辑。
此时传入的type
参数类型符合定义的接口interface ClassComponent
:
-
data
函数 -
mounted
函数
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 {
...
return vnode
}
此时返回的VNode:
render
在得到了VNode后,执行了render(vnode, rootContainer)
。
render
函数是在 packages/runtime-core/src/renderer.js
中函数baseCreateRenderer
闭包内部声明的。
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
内部调用了patch
方法完成对VNode
的解析与渲染
const patch: PatchFn = (
n1,
n2,
container,
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
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
/*...*/
default:
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
)
}
/*...*/
}
}
在这个switch
语句中会根据vnode
中shapeFlag
字段对不同类型的vnode
进行处理。
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
}
通过位运算符>>
、|
定义了枚举类型 ShapeFlags
; 在逻辑语句中使用&
位运算符进行匹配;
左移:a << b 将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。
按位或:a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
按位与:a & b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。
例如当前的demo:_createVNode
方法在生成根组件VNode
时,
属性赋值操作:shapeFlag = ShapeFlags.STATEFUL_COMPONENT
;
因此会匹配到ShapeFlags.COMPONENT
;
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
并执行了方法processComponent
;
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
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)
}
}
在判断当前组件不是keep-alive
类型后继续执行了mountComponent
方法
mountComponent
下面详细看mountComponent
方法中的省略后的核心逻辑:
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const instance = createComponentInstance(initialVNode, parentComponent, parentSuspense)
setupComponent(instance)
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
创建组件实例
createComponentInstance
初始化并return
了instance
对象,
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance = {/*...*/}
if (__DEV__) {
instance.ctx = createRenderContext(instance)
} else {
instance.ctx = { _: instance }
}
instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
return instance
}
其中instance.ctx
做了一层引用instance.ctx = { _: instance }
setupComponent: 解析props、slots、setup
创建instance
后,然后执行了setupComponent(instance)
setupComponent
核心代码
function setupComponent (instance) {
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = setupStatefulComponent(instance, isSSR)
return setupResult
}
initProps
:初始化props
、将其处理为响应式的
initSlots
: 处理插槽
setupStatefulComponent
: 处理setup配置选项
解析模板、初始化选项
setupStatefulComponent
简略后的逻辑:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 创建渲染代理属性访问缓存
instance.accessCache = {}
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2. call setup()
const { setup } = Component
if (setup) {
const setupResult = setup()
if (isPromise(setupResult)) {
return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
} else {
handleSetupResult(instance, setupContext)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
如果组件选项里配置了setup
,则会调用handleSetupResult
对返回值setupResult
进行处理;
setupStatefulComponent
内最终逻辑都会调用finishComponentSetup
方法:
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
applyOptions(instance, Component)
}
解析模板: 将
template
模板转换为render
函数;
备注:compile
函数是在packages/vue/src/index.js中调用registerRuntimeCompiler(compileToFunction)
完成的编译器注册执行
applyOptions
方法。并在初始化选项前调用beforeCreate
钩子:
选项初始化顺序保持了与Vue 2的一致:
- props (上一步
initProps
中已经完成了初始化) - inject
- methods
- data (由于它依赖于
this
访问而推迟) - computed
- watch (由于它依赖于
this
访问而推迟)
完成上面初始化后调用了 created
钩子,然后注册其余声明周期钩子:
beforeMount?(): void
mounted?(): void
beforeUpdate?(): void
updated?(): void
activated?(): void
deactivated?(): void
/** @deprecated use `beforeUnmount` instead */
beforeDestroy?(): void
beforeUnmount?(): void
/** @deprecated use `unmounted` instead */
destroyed?(): void
unmounted?(): void
renderTracked?: DebuggerHook
renderTriggered?: DebuggerHook
errorCaptured?: ErrorCapturedHook
到此setupComponent
函数结束,下面继续执行setupRenderEffect
setupRenderEffect:为渲染创建响应性效果
下面是setupRenderEffect
的伪代码:
const setupRenderEffect = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) {
instance.update = effect(function componentEffect() {
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
initialVNode.el = subTree.el
}, EffectOptions)
}
调用了命名为effect
的函数,第一个参数接收一个函数,第二个参数是配置选项;并将函数执行结果赋值给instance.update
;
componentEffect
回调中得到子节点的VNode,并递归调用了patch
方法。
通过initialVNode.el = subTree.el
实现dom的挂载
何时创建的dom?
通过递归执行patch
方法,会遍历整个vnode
树;
这个过程中非dom
节点类类型的Vnode
会重复上面的过程,继续调用patch
递归函数最终会结束,那么这个函数内一定有一个分支不再调用自己。这个就是普通vnode
节点
举例:当Vnode
类型是Text
,会走到处理逻辑processText
方法中
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
hostCreateText
创建了Text dom
。该方法定义在packages/runtime-dom/src/nodeOps.ts
const createText = document.createTextNode(text)