搭建調(diào)試環(huán)境
為了弄清楚 Vue3 的初始化,建議先克隆 Vue3 到本地。
git clone https://github.com/vuejs/vue-next.git
安裝依賴
npm install
修改 package.json,將 dev 命令加上 --sourcemap 方便調(diào)試,并運(yùn)行 npm run dev
// package.json
...
"scripts": {
"dev": "node scripts/dev.js --sourcemap",
...
}
...
在 packages/vue 目錄下增加 index.html,內(nèi)容如下
<!-- index.html -->
<div id="app">
{{ count }}
</div>
<script src="./dist/vue.global.js"></script>
<script>
Vue.createApp({
setup() {
const count = Vue.ref(0);
return { count };
}
}).mount('#app');
</script>
在瀏覽器打開 index.html,程序正常運(yùn)行則可以開始進(jìn)行下一步調(diào)試。
進(jìn)行調(diào)試
假如在下面的流程中迷失了方向,建議先看一下結(jié)尾的總結(jié),再回過頭來看這一段。

在 createApp 的位置打上斷點,然后刷新頁面進(jìn)斷點,開始調(diào)試。
具體大家可以自行調(diào)試,我在這里就大概描述一下初始化流程。
createApp
進(jìn)入 createApp 內(nèi)部,會跳轉(zhuǎn)到 packages/runtime-dom/src/index.ts 的 createApp,執(zhí)行完成返回 app 實例。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
接下來繼續(xù)看 ensureRenderer 的實現(xiàn)。
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
這里的 renderer 是個單例,初始時會調(diào)用 createRenderer 創(chuàng)建,再繼續(xù)深入。
來到 packages/runtime-core/src/renderer.ts,可以看到 createRenderer 又會調(diào)用 baseCreateRenderer。
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
baseCreateRenderer 的實現(xiàn)有 2000 行,我們只需要關(guān)注幾個關(guān)鍵點就可以了。

其返回值也就是 ensureRenderer() 的返回值
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此處省略 2000 行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
接下來回到開始的位置,在執(zhí)行完 ensureRenderer() 后會接著執(zhí)行 createApp,這個 createApp 就是上一步返回的 createApp,再接著看看做了哪些工作。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
ensureRenderer 返回的 createApp 由 createAppAPI 實現(xiàn),接下來再看看 createAppAPI 是如何實現(xiàn)的吧。
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {},
set config(v) {},
use(plugin: Plugin, ...options: any[]) {},
mixin(mixin: ComponentOptions) {},
component(name: string, component?: Component): any {},
directive(name: string, directive?: Directive) {},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {},
unmount() {},
provide(key, value) {}
})
return app
}
}
可以看到 createAppAPI 會返回一個 createApp 函數(shù),也就是我們調(diào)用 createApp,當(dāng) createApp 執(zhí)行完之后會返回 app 實例。app 實例還會有 use、mixin、component、directive 等方法,可以為全局 app 添加一些擴(kuò)展。
案例如下,傳入的第一個參數(shù)為上一步的 rootComponent 也就是根組件。
// index.html
const app = Vue.createApp({})
.use(xxx)
.component(xxx)
.mount(xxx)
mount
createApp 創(chuàng)建 app 實例后,要渲染到頁面上還需要調(diào)用 mount。接下來看看 mount 又做了什么工作。還是在 createAppAPI 內(nèi)部
// packages/runtime-core/src/apiCreateApp.ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
...
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
...
return vnode.component!.proxy
} else if (__DEV__) {
// 開發(fā)環(huán)境警告提醒,app 不可以重復(fù)掛載
}
}
最終會執(zhí)行 render(vnode, rootContainer, isSVG) 這一行代碼,接下來看看調(diào)用 createAppAPI 時傳入的 renderer。
回到 baseCreateRenderer 中,可以看到在 return 時調(diào)用 createAppAPI 傳入的 renderer。
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此處省略 2000 行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
其中 renderer 在 baseCreateRenderer 中定義了
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
}
在 index.html 的例子中,第一次執(zhí)行 render 時 vnode 是由 rootComponent 創(chuàng)建出來,rootComponent 則是 createApp 時傳入的對象。
container 為 app 容器,也就是 id 為 app 的 div , container._vnode 為 undefined。
所以最終會進(jìn)入 patch 。
patch 的邏輯同樣位于 baseCreateRenderer 中。代碼太長了,這里就講一下思路。在 patch 中會判斷 vnode 的 type 或 shapeFlag 執(zhí)行對應(yīng)的操作。

因為第一次 patch 時,vnode 是一個組件,會進(jìn)入 ShapeFlags.COMPONENT 的判斷內(nèi),執(zhí)行 processComponent進(jìn)行組件的處理。
然后會觸發(fā) mountComponent 掛載組件,從而觸發(fā) setupComponent(instance) 初始化組件的 props、slots、setup 等將需要 proxy 代理的數(shù)據(jù)做好準(zhǔn)備,以及將 template 進(jìn)行編譯為 render。
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-creaate the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
...
setupComponent(instance)
...
}
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
...
}
緊接著會執(zhí)行 setupRenderEffect,該方法會將渲染函數(shù)封裝成一個副作用,當(dāng)依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時,會自動重新執(zhí)行。
重點關(guān)注 componentUpdateFn,代碼太長了,這里也簡單講下吧,第一次會執(zhí)行 instance.isMounted 為 undefined,則會進(jìn)入創(chuàng)建流程,其中會執(zhí)行 const subTree = (instance.subTree = renderComponentRoot(instance)) 創(chuàng)建子樹,然后通過 patch 遞歸創(chuàng)建子節(jié)點,結(jié)束后 instance.isMounted = true。
一旦依賴發(fā)生變化,componentUpdateFn會被重新執(zhí)行,instance.isMounted 為 true,則會盡心更新的處理,具體就不再展開了。
至于為什么依賴發(fā)生變化 componentUpdateFn會被重新執(zhí)行,這個我們留在下一篇文章中介紹,記得關(guān)注我。
總結(jié)
- 在 index.html 調(diào)用
createApp時會先經(jīng)過ensureRenderer和baseCreateRenderer生成下面的對象
// baseCreateRenderer 返回值
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
- 繼續(xù)調(diào)用
baseCreateRenderer返回的createApp,這里的createApp實際上調(diào)用的是createAppAPI返回的函數(shù)。
createAppAPI執(zhí)行完成返回 app 實例。
- index.html 在創(chuàng)建好 app 后接著調(diào)用
mount進(jìn)行掛載,mount的實現(xiàn)在createAppAPI內(nèi)部。
mount 執(zhí)行時會調(diào)用 render函數(shù),該 render 在 baseCreateRenderer 傳入。
-
render則開始patch進(jìn)行渲染,patch內(nèi)部會進(jìn)行遞歸渲染子節(jié)點。
以上就是 Vue3 的 createApp 和 mount 的大致流程。至于第一步為什么需要經(jīng)過 ensureRenderer 和 baseCreateRenderer?
baseCreateRenderer 主要是平臺無關(guān)的邏輯處理,存放在 runtime-core 中。
當(dāng) patch 的時候需要操作 dom,則會調(diào)用外部傳入的方法進(jìn)行操作,這樣就可以更方便實現(xiàn)跨端。
ensureRenderer 存放在 runtime-dom 中,主要為 baseCreateRenderer 提供一系列 dom 操作的函數(shù)。
假如我們要自定義渲染器,那么只需要實現(xiàn)ensureRenderer 即可。而不是像 Vue2 需要 fork 一份,大大提高了 Vue3 的應(yīng)用范圍。
好了,這篇文章就水到這里吧。如有錯誤的地方,希望還能在評論區(qū)指出,感謝!
下篇文章將解析 Vue3 的響應(yīng)式原理,如果有興趣的話別忘了關(guān)注我呀,我們一起學(xué)習(xí)、進(jìn)步。