搭建調試環境
爲了弄清楚 Vue3 的初始化,建議先克隆 Vue3 到本地。
git clone https://github.com/vuejs/vue-next.git
安裝依賴
npm install
修改 package.json,將 dev 命令加上 --sourcemap
方便調試,並運行 npm run dev
// package.json
...
"scripts": {
"dev": "node scripts/dev.js --sourcemap",
...
}
...
在 packages/vue 目錄下增加 index.html,內容如下
<!-- 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,程序正常運行則可以開始進行下一步調試。
進行調試
假如在下面的流程中迷失了方向,建議先看一下結尾的總結,再回過頭來看這一段。
在 createApp 的位置打上斷點,然後刷新頁面進斷點,開始調試。
具體大家可以自行調試,我在這裏就大概描述一下初始化流程。
createApp
進入 createApp 內部,會跳轉到 packages/runtime-dom/src/index.ts 的 createApp,執行完成返回 app 實例。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
接下來繼續看 ensureRenderer 的實現。
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
這裏的 renderer 是個單例,初始時會調用 createRenderer 創建,再繼續深入。
來到 packages/runtime-core/src/renderer.ts,可以看到 createRenderer 又會調用 baseCreateRenderer。
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
baseCreateRenderer 的實現有 2000 行,我們只需要關注幾個關鍵點就可以了。
其返回值也就是 ensureRenderer() 的返回值
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此處省略 2000 行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
接下來回到開始的位置,在執行完 ensureRenderer() 後會接着執行 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 實現,接下來再看看 createAppAPI 是如何實現的吧。
// 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 函數,也就是我們調用 createApp,當 createApp 執行完之後會返回 app 實例。app 實例還會有 use、mixin、component、directive 等方法,可以爲全局 app 添加一些擴展。
案例如下,傳入的第一個參數爲上一步的 rootComponent 也就是根組件。
// index.html
const app = Vue.createApp({})
.use(xxx)
.component(xxx)
.mount(xxx)
mount
createApp 創建 app 實例後,要渲染到頁面上還需要調用 mount。接下來看看 mount 又做了什麼工作。還是在 createAppAPI 內部
// 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__) {
// 開發環境警告提醒,app 不可以重複掛載
}
}
最終會執行 render(vnode, rootContainer, isSVG)
這一行代碼,接下來看看調用 createAppAPI 時傳入的 renderer。
回到 baseCreateRenderer 中,可以看到在 return 時調用 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 的例子中,第一次執行 render 時 vnode 是由 rootComponent
創建出來,rootComponent 則是 createApp 時傳入的對象。
container
爲 app 容器,也就是 id 爲 app 的 div , container._vnode
爲 undefined
。
所以最終會進入 patch 。
patch 的邏輯同樣位於 baseCreateRenderer 中。代碼太長了,這裏就講一下思路。在 patch 中會判斷 vnode 的 type 或 shapeFlag 執行對應的操作。
因爲第一次 patch 時,vnode 是一個組件,會進入 ShapeFlags.COMPONENT
的判斷內,執行 processComponent
進行組件的處理。
然後會觸發 mountComponent
掛載組件,從而觸發 setupComponent(instance)
初始化組件的 props、slots、setup 等將需要 proxy 代理的數據做好準備,以及將 template 進行編譯爲 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
)
...
}
緊接着會執行 setupRenderEffect
,該方法會將渲染函數封裝成一個副作用,當依賴的響應式數據發生變化時,會自動重新執行。
重點關注 componentUpdateFn
,代碼太長了,這裏也簡單講下吧,第一次會執行 instance.isMounted
爲 undefined,則會進入創建流程,其中會執行 const subTree = (instance.subTree = renderComponentRoot(instance))
創建子樹,然後通過 patch 遞歸創建子節點,結束後 instance.isMounted = true
。
一旦依賴發生變化,componentUpdateFn
會被重新執行,instance.isMounted
爲 true,則會盡心更新的處理,具體就不再展開了。
至於爲什麼依賴發生變化 componentUpdateFn
會被重新執行,這個我們留在下一篇文章中介紹,記得關注我。
總結
- 在 index.html 調用
createApp
時會先經過ensureRenderer
和baseCreateRenderer
生成下面的對象
// baseCreateRenderer 返回值
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
- 繼續調用
baseCreateRenderer
返回的createApp
,這裏的createApp
實際上調用的是createAppAPI
返回的函數。
createAppAPI
執行完成返回 app 實例。
- index.html 在創建好 app 後接着調用
mount
進行掛載,mount
的實現在createAppAPI
內部。
mount
執行時會調用 render
函數,該 render
在 baseCreateRenderer
傳入。
-
render
則開始patch
進行渲染,patch
內部會進行遞歸渲染子節點。
以上就是 Vue3 的 createApp 和 mount 的大致流程。至於第一步爲什麼需要經過 ensureRenderer
和 baseCreateRenderer
?
baseCreateRenderer
主要是平臺無關的邏輯處理,存放在 runtime-core 中。
當 patch
的時候需要操作 dom,則會調用外部傳入的方法進行操作,這樣就可以更方便實現跨端。
ensureRenderer
存放在 runtime-dom 中,主要爲 baseCreateRenderer
提供一系列 dom 操作的函數。
假如我們要自定義渲染器,那麼只需要實現ensureRenderer
即可。而不是像 Vue2 需要 fork 一份,大大提高了 Vue3 的應用範圍。
好了,這篇文章就水到這裏吧。如有錯誤的地方,希望還能在評論區指出,感謝!
下篇文章將解析 Vue3 的響應式原理,如果有興趣的話別忘了關注我呀,我們一起學習、進步。