先看一下vue-next官方文檔的介紹:
每個 Vue 應(yīng)用都是通過用
createApp函數(shù)創(chuàng)建一個新的應(yīng)用實例開始的
傳遞給
createApp的選項用于配置根組件。當我們掛載應(yīng)用時,該組件被用作渲染的起點。
一個應(yīng)用需要被掛載到一個 DOM 元素中。例如,如果我們想把一個 Vue 應(yīng)用掛載到
<div id="app"></div>,我們應(yīng)該傳遞#app
我們將分為兩部分進行渲染過程的理解:
- 創(chuàng)建應(yīng)用實例,函數(shù)
createApp的剖析 - 應(yīng)用實例掛載, 函數(shù)
mount方法掛載過程
本篇詳細講述調(diào)用應(yīng)用實例方法mount過程
下面是一個簡單的demo
<!-- template -->
<div id="app">
<input v-model="value"/>
<p>雙向綁定:{{value}}</p>
<hello-comp person-name="zhangsan"/>
</div>
const { createApp } = Vue
const helloComp = {
name: 'hello-comp',
props: {
personName: {
type: String,
default: 'wangcong'
}
},
template: '<p>hello {{personName}}!</p>'
}
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')
現(xiàn)在我們從mount函數(shù)為入口,去了解應(yīng)用掛載的過程。

掛載 mount
現(xiàn)在回顧一下在demo中我們調(diào)用的方法createApp(app).mount('#app')
此時調(diào)用的這個mount方法是在runtime-dom模塊重寫之后的,在內(nèi)部依然會執(zhí)行應(yīng)用實例的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是調(diào)用了mount方法返回的:const proxy = mount(container)
現(xiàn)在看一下在應(yīng)用實例app中定義的mount方法,它做了兩件事情:
- 執(zhí)行
createVNode方法生產(chǎn)VNode - 執(zhí)行
render方法。(上一篇文中所提:父級作用域函數(shù)createAppAPI(render, hydrate)接受的第一個參數(shù))
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<Node, Element>, 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 // 靜態(tài)組件 純html
| typeof Comment
| typeof Fragment // 多根組件
| typeof TeleportImpl // 內(nèi)置組件傳送
| typeof SuspenseImpl // 內(nèi)置組件懸念 一般配合異步組件
createVNode函數(shù)內(nèi)部實際會調(diào)用_createVNode函數(shù)
export const createVNode = (__DEV__
? createVNodeWithArgsTransform
: _createVNode) as typeof _createVNode
我們傳入createApp方法的根組件對象會作為_createVNode方法接收的第一個參數(shù)type,并在執(zhí)行后會返回一個VNode。VNode中包含一個shapeFlag標識類型,在后面patch過程中進入不同的處理邏輯。
此時傳入的type參數(shù)類型符合定義的接口interface ClassComponent:
-
data函數(shù) -
mounted函數(shù)
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后,執(zhí)行了render(vnode, rootContainer)。
render函數(shù)是在 packages/runtime-core/src/renderer.js 中函數(shù)baseCreateRenderer閉包內(nèi)部聲明的。
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
}
內(nèi)部調(diào)用了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語句中會根據(jù)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 對于每一個比特位,當兩個操作數(shù)相應(yīng)的比特位至少有一個1時,結(jié)果為1,否則為0。
按位與:a & b 對于每一個比特位,只有兩個操作數(shù)相應(yīng)的比特位都是1時,結(jié)果才為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
并執(zhí)行了方法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類型后繼續(xù)執(zhí)行了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)
}
創(chuàng)建組件實例
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
創(chuàng)建instance后,然后執(zhí)行了setupComponent(instance)
setupComponent核心代碼
function setupComponent (instance) {
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = setupStatefulComponent(instance, isSSR)
return setupResult
}
initProps:初始化props、將其處理為響應(yīng)式的
initSlots: 處理插槽
setupStatefulComponent: 處理setup配置選項
解析模板、初始化選項
setupStatefulComponent簡略后的邏輯:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 創(chuàng)建渲染代理屬性訪問緩存
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,則會調(diào)用handleSetupResult對返回值setupResult進行處理;
setupStatefulComponent內(nèi)最終邏輯都會調(diào)用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模板轉(zhuǎn)換為render函數(shù);
備注:compile函數(shù)是在packages/vue/src/index.js中調(diào)用registerRuntimeCompiler(compileToFunction)完成的編譯器注冊執(zhí)行
applyOptions方法。并在初始化選項前調(diào)用beforeCreate鉤子:
選項初始化順序保持了與Vue 2的一致:
- props (上一步
initProps中已經(jīng)完成了初始化) - inject
- methods
- data (由于它依賴于
this訪問而推遲) - computed
- watch (由于它依賴于
this訪問而推遲)
完成上面初始化后調(diào)用了 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函數(shù)結(jié)束,下面繼續(xù)執(zhí)行setupRenderEffect
setupRenderEffect:為渲染創(chuàng)建響應(yīng)性效果
下面是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)
}
調(diào)用了命名為effect的函數(shù),第一個參數(shù)接收一個函數(shù),第二個參數(shù)是配置選項;并將函數(shù)執(zhí)行結(jié)果賦值給instance.update;
componentEffect回調(diào)中得到子節(jié)點的VNode,并遞歸調(diào)用了patch方法。
通過initialVNode.el = subTree.el實現(xiàn)dom的掛載
何時創(chuàng)建的dom?
通過遞歸執(zhí)行patch方法,會遍歷整個vnode樹;
這個過程中非dom節(jié)點類類型的Vnode會重復上面的過程,繼續(xù)調(diào)用patch
遞歸函數(shù)最終會結(jié)束,那么這個函數(shù)內(nèi)一定有一個分支不再調(diào)用自己。這個就是普通vnode節(jié)點
舉例:當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創(chuàng)建了Text dom。該方法定義在packages/runtime-dom/src/nodeOps.ts
const createText = document.createTextNode(text)