前言:
在源碼分析概述中, 我們對源碼源碼結(jié)構(gòu)進(jìn)行了分析, 整個vue
三大核心系統(tǒng), 即響應(yīng)式系統(tǒng)
, 渲染系統(tǒng)
,編譯系統(tǒng)
. 而響應(yīng)式系統(tǒng)
和渲染系統(tǒng)
又共同構(gòu)建了vue
官網(wǎng)上所謂的運行時構(gòu)建
vue3
源碼中的三大系統(tǒng)有明確的分界, 但在使用時, 通常并不會單獨區(qū)分使用某一個系統(tǒng), vue3
的應(yīng)用是在三個系統(tǒng)相互作用的結(jié)果, 至少是運行時, 即包括渲染系統(tǒng)
和響應(yīng)式系統(tǒng)
.
所以我的對vue3
源碼分析會根據(jù)需要交替分析三大系統(tǒng)中對應(yīng)的源碼
在vue3
中, 我們需要使用createApp
API 來創(chuàng)建應(yīng)用實例, 然后通過創(chuàng)建的實例調(diào)用mount
方法將應(yīng)用掛載到DOM
節(jié)點上, 因此createApp
可以理解為整個vue3
應(yīng)用的入口, 包括官網(wǎng)API都將createApp
排列在第一位
所以今天這篇文章, 主要針對createApp
創(chuàng)建vue3
應(yīng)用的API 進(jìn)行源碼分析.
在分析之前, 我先放一張createApp
初始化應(yīng)用的流程圖:
createApp
初始化流程圖如下:
不知道為什么簡書一直傳不上去圖片, 如果大家想看流程圖, 可以去我的
CSDN
博客中看,地址給到大家
CSDN博客
如果你此時看到該流程圖是一個呆萌的狀態(tài), 可以在看完該文章后在回頭來看一下該流程圖, 你就會非常清楚的知道createApp
函數(shù)都做了什么
好, 接下來我們開始正式分析createApp
創(chuàng)建應(yīng)用API的具體實現(xiàn), 那接下請大家跟著我一起探尋一下createApp
API 源碼內(nèi)部邏輯, 看看源碼中都做了些什么.
1. 初始化應(yīng)用
我們使用一個具體的實例作為切入點, 通過斷點調(diào)試的方式對createApp
源碼進(jìn)行分析.理解vue3
初始化的過程.
實例如下:
const { createApp } = Vue
const app = createApp({
template: '<h1>hello</h1>'
})
app.mount('#app')
這是一個最簡單的實例, 在實例中, 我們首先通過解構(gòu)的方式獲取createApp
方法.
通過createApp
方法創(chuàng)建應(yīng)用, 并通過mount
方法將應(yīng)用掛載到DOM
上
接下來我們將通過這個實例分析createApp
創(chuàng)建vue3
應(yīng)用的API , 以及mount
掛載方法.
2. 創(chuàng)建vue3應(yīng)用的源碼分析
2.1 定位 createApp 方法
我們知道vue3
源碼采用pnpm
多包管理, 目前我們只知道createApp
API 是從vue
中結(jié)構(gòu)出來的, 那么該API具體來自于哪個包中, 現(xiàn)在并不知道.
因此在分析createApp
API 前, 我們需要先確認(rèn)該API 來自于哪個包.
我們可以通過斷點調(diào)試的分時來分析, 使用單步調(diào)試, 進(jìn)入到createApp
方法內(nèi)
此時就可以看到createApp
方法來自于packages/runtime-dom
包中
runtime
是運行時.其中包括runtime-dom
,runtime-core
兩個包
-
runtime-dom
處理與瀏覽器相關(guān)的dom 操作的API -
runtime-core
是運行時核心代碼,與渲染平臺無關(guān)
也就意味著runtime-dom
的運行依賴于runtime-core
包, 所以在runtime-dom/src/index.ts
模塊中一定會引入runtime-core
包
import { /.../ } from '@vue/runtime-core'
// 創(chuàng)建 vue 應(yīng)用API
export const createApp = ((...args) => {
// ...
}) as CreateAppFunction<Element>
2.2 createApp 函數(shù)分析
在確定createApp
函數(shù)后, 接下來就是分析該方法的實現(xiàn)
源碼:
export const createApp = ((...args) => {
// 1. 首先創(chuàng)建應(yīng)用
const app = ensureRenderer().createApp(...args)
// 2. 重寫mount 方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// ...后面分析
}
//3.返回應(yīng)用
return app
}) as CreateAppFunction<Element>
這里我對源碼進(jìn)行了精簡, 這樣非常方便的可以看出.createApp
函數(shù)的核心邏輯就是創(chuàng)建Vue3
應(yīng)用對象app
并返回, 至于重寫mount
方法, 稍后我會帶大家具體分析
我們先看下createApp
的入?yún)? 用過createApp
API的朋友, 相信大家都知道, createApp
接收兩個入?yún)?/p>
-
根組件
: 根組件可以是單文件組件, 也可以是組件的選項對象 -
props
: 向根組件傳入的props數(shù)據(jù), 該參數(shù)為可選參數(shù).
在源碼中可以看出, createApp
函數(shù)通過...args
剩余運算符收集所有入?yún)? 將參數(shù)組合成為數(shù)組. 如果傳入了第二個參數(shù)props
, args
數(shù)組收集到的數(shù)據(jù)將會有兩項, 否則將只有一項, 即根組件對象.
所以createApp
函數(shù)會接收根組件作為參數(shù), 并且返回vue
應(yīng)用, 即方法內(nèi)app
就是createApp
創(chuàng)建的應(yīng)用對象
我相信大家已經(jīng)看出來了, createApp
函數(shù)中創(chuàng)建的應(yīng)用對象app
其本質(zhì)是通過ensureRenderer().createApp(...args)
創(chuàng)建的
通過這個結(jié)構(gòu)不難看出: ensureRenderer()
方法調(diào)用完畢后返回一個包含createApp
方法的對象, 并通過該方法的調(diào)用創(chuàng)建vue
應(yīng)用
2.3 ensureRenderer 確認(rèn)渲染器
接下來我們需要先確認(rèn)ensureRenderer
確認(rèn)渲染器函數(shù)
源碼:
// Renderer 為Web前端渲染器 , HydrationRenderer為SSR服務(wù)器端渲染使用,
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
通過ensureRenderer
函數(shù)內(nèi)部邏輯不難看出,ensureRenderer
函數(shù)的作用就是用來確定渲染器對象(renderer)
存在.
如果渲染器renderer
不存在, 就會調(diào)用createRenderer
函數(shù)創(chuàng)建渲染器, 根據(jù)前面的分析, 渲染器將是一個包含createApp
方法的對象
初始創(chuàng)建vue
應(yīng)用renderer
值為必然為空, 會調(diào)用createRenderer
函數(shù), 并傳入?yún)?shù)rendererOptions
這里rendererOptions
參數(shù)是渲染器配置對象, 主要作用就是用來操作DOM的一些方法
源碼:
// 渲染器配置對象
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
// extend 就是合并對象的方法(shared/src/general.ts)
export const extend = Object.assign
// dom 操作(runtime-core/src/nodeOps.ts )
const doc = (typeof document !== 'undefined' ? document : null) as Document
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// 插入節(jié)點
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
// 創(chuàng)建文本節(jié)點
createText: text => doc.createTextNode(text),
// 創(chuàng)建注釋節(jié)點
createComment: text => doc.createComment(text),
//...
}
這里將幾個模塊中的中的方法放在了一起, 很方便的看出, rendererOptions
渲染器配置對象就是封裝了DOM操作方法的對象, 由兩部分組成:
-
patchProp
:處理元素的props
砸西、Attribute
影所、class
、style
、event
事件等伦连。 -
nodeOps
:處理DOM節(jié)點指蚁,這個對象里面包含了各種封裝原生DOM操作的方法。
rendererOptions
對象最終會傳遞到渲染器里面颈娜,里面的各種方法最終會在頁面渲染的過程中被調(diào)用剑逃。
至此我們暫時了解如下幾件事:
-
createApp
方法的作用就是創(chuàng)建Vue3
應(yīng)用. -
createApp
方法內(nèi)真正創(chuàng)建Vue3
應(yīng)用的是渲染器對象的createApp
方法 -
createRenderer
方法的作用是用來創(chuàng)建渲染器對象, 接收rendererOptions
渲染器配置對象 -
rendererOptions
配置對象中包含DOM操作的方法
因此要創(chuàng)建Vue3
應(yīng)用必須先創(chuàng)建渲染器對象, 因此接下來我們繼續(xù)分析createRenderer
方法的實現(xiàn)
2.4 createRenderer 創(chuàng)建渲染器方法
createRenderer
方法來源于runtime-core/src/render.ts
模塊
具體源碼實現(xiàn)如下
// 創(chuàng)建渲染器方法
function createRenderer(options) {
return baseCreateRenderer(options)
}
// 真正創(chuàng)建渲染器函數(shù)
function baseCreateRenderer(options,createHydrationFns) {
// ... 省略渲染方法
// 渲染函數(shù)
const render = (vnode, container, isSVG) => {
//...
}
let hydrated;
let hydrateNode;
if (createHydrationFns) {
;[hydrate, hydrateNode] = createHydrationFns(internals)
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
通過源碼可以看出createRenderer
函數(shù)返回baseCreateRenderer
方法執(zhí)行的結(jié)果, 即真正創(chuàng)建渲染器對象的函數(shù)是baseCreateRenderer
baseCreateRenderer
創(chuàng)建的渲染器對象包括render
, hydrate
, createApp
三個方法
獲取到渲染器對象后, 就會通過渲染器對象調(diào)用createApp
方法創(chuàng)建應(yīng)用. 而createApp
方法的值是通過createApp
API方法創(chuàng)建的, 同時會將render
, hydrate
,兩個方法作為參數(shù)傳入createApp
API方法中.
所以接下來我們需要看一下createApp
API方法的實現(xiàn)
2.5 createApp
API 方法
createApp
API方法來自于runtime-core/src/apiCreateApp.ts
具體實現(xiàn)如下
let uid = 0
export function createAppAPI(render,hydrate){
// 返回創(chuàng)建應(yīng)用的createApp 方法(利用閉包緩存render, hydrate 參數(shù))
return function createApp(rootComponent, rootProps = null) {
// rootComponent 就是傳入的根組件
// rootProps 為向根組件傳入props 數(shù)據(jù)
// 1. 如果根組件不是函數(shù), 則進(jìn)行一次淺拷貝
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
// 2.rootProps 為傳入根組件的props , 參數(shù)必須是一個對象或null
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 3. 創(chuàng)建vue應(yīng)用實例上下文對象, 就是一些默認(rèn)配置
const context = createAppContext()
// 4. 初始化插件集合, 記錄通過use 創(chuàng)建的插件
const installedPlugins = new WeakSet()
// 5. 始化應(yīng)用掛載狀態(tài),默認(rèn)為false
let isMounted = false
// 6. 創(chuàng)建 vue 應(yīng)用實例對象
// 同時將應(yīng)用實例存儲到context上下文的app屬性
const app: App = (context.app = {
// 初始化應(yīng)用屬性
_uid: uid++, // 項目中可能存在多個vue實例官辽,需使用id標(biāo)識
_component: rootComponent as ConcreteComponent, // 根組件
_props: rootProps, // 傳遞給根組件的props
_container: null, // 掛載點: DOM容器
_context: context, // app上下文
_instance: null, // 應(yīng)用實例
version, // 版本
// 定義了一個訪問器屬性app.config蛹磺,只能讀取,不能直接替換
get config() {return context.config},
set config(v) {},
// 掛載插件, 返回app對象
use(plugin, ...options) {/*... 省略代碼*/ return app },
// 混入
mixin(mixin) { /*... 省略代碼*/ return app },
// 注冊全局組件
component(name, component) { /*... 省略代碼*/ return app },
// 定義全局指令
directive(name, directive) { /*... 省略代碼*/ return app },
// 應(yīng)用掛載/卸載方法
mount( rootContainer,isHydrate,isSVG ) {/*... 省略代碼*/},
unmount() { /*... 省略代碼*/ },
// 全局注入方法
provide(key, value) {/*... 省略代碼*/ return app },
runWithContext(fn) {/*... 省略代碼*/ }
})
// __COMPAT__ 判斷是否兼容vue2
// 若開啟兼容同仆,安裝vue2相關(guān)的API
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
// 返回 app 對象
return app
}
}
源碼中createApp
API函數(shù)返回了一個函數(shù), 返回的這個函數(shù)才是真正的createApp
函數(shù).
因此該返回的函數(shù)就是最后用來創(chuàng)建應(yīng)用對象的createApp
方法
分析至此,我們就可以知道了
-
createApp
API 創(chuàng)建應(yīng)用實例對象并返回 -
createApp
API 中創(chuàng)建應(yīng)用對象, 是通過調(diào)用渲染器{render,hydrate,createApp}
中的createApp
方法創(chuàng)建的 - 渲染器中的
createApp
方法則是createApp
API函數(shù)的返回函數(shù)
而createApp
創(chuàng)建的應(yīng)用對象就是包含了一些屬性和常用的use
,mixins
,component
,directive
,mount
,unmount
,provide
方法的對象. 因此可以通過app
應(yīng)用對象調(diào)用moun
t掛載應(yīng)用
3. mount 掛載源碼分析
在上面我們已經(jīng)分析了createApp
函數(shù), 在調(diào)用完畢后返回vue
應(yīng)用實例對象, 在使用時會通過應(yīng)用實例對象調(diào)用mount
方法掛載應(yīng)用.
3.1 確定mount 方法
通過前面的分析,我們已經(jīng)明白, 創(chuàng)建應(yīng)用實例的createApp
函數(shù)是通過調(diào)用createApp
API方法返回的createApp
創(chuàng)建的應(yīng)用實例. 在此方法中可以看到生成應(yīng)用對象app
具有mount
方法
源碼:
export function createAppAPI(render,hydrate){
// 創(chuàng)建vue應(yīng)用實例的方法
return function createApp(rootComponent, rootProps = null) {
//...
// 應(yīng)用實例對象
const app: App = (context.app = {
//..
// 應(yīng)用實例對象掛載mount 方法
mount(rootContainer,isHydrate,isSVG) {
// ...
},
//...
})
//..
// 返回應(yīng)用實例對象
return app
}
}
但我們調(diào)用app.mount()
掛載應(yīng)用時并不是調(diào)用該mount
方法, 原因在于createApp
方法中對當(dāng)前mount
方法進(jìn)行了重寫
export const createApp = ((...args) => {
// 1. 首先創(chuàng)建應(yīng)用
const app = ensureRenderer().createApp(...args)
// 2. 重寫mount 方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// ...后面分析
}
//3.返回應(yīng)用
return app
}) as CreateAppFunction<Element>
源碼很清楚的表明,createApp
函數(shù)中創(chuàng)建完應(yīng)用實例對象, 接著通過解構(gòu)的方式將應(yīng)用實例對象app
的mount
方法記錄在變量mount
上
然后重寫app
對象的mount
方法進(jìn)行重寫, 賦值了一個新的方法
也就是在在我們通過app.mount()
掛載應(yīng)用時, 進(jìn)入的是createApp
函數(shù)中對app
應(yīng)用對象重寫的mount
方法
3.2 mount 掛載
在實例中,我們調(diào)用mount
方法進(jìn)行掛載時,傳入了一個字符串#app
, 以此獲取到掛載點DOM元素
app.mount('#app')
mount
具體是如何掛載的呢?, 我們首先分析一下createApp
函數(shù)中重寫的mount
方法
重寫的mount方法
export const createApp = ((...args) => {
// 創(chuàng)建應(yīng)用實例對象
const app = ensureRenderer().createApp(...args)
// 重寫mount 方法
// 備份mount 方法
const { mount } = app
// 重寫mount 方法
app.mount = (containerOrSelector: Element | ShadowRoot | string) => {
// 1. 獲取掛載容器(dom元素)
const container = normalizeContainer(containerOrSelector)
// 沒有容器直接return 無法掛載應(yīng)用
if (!container) return
// 2. 處理根組件
// 通過應(yīng)用對象獲取根組件, 即createApp() 方法第一個參數(shù)
const component = app._component
// 驗證根組件是一個對象,且不具有render, template屬性, 則使用掛載點內(nèi)容作為模板
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
// 需要確保掛載點模板是可信的, 因為模板中可能存在JS表達(dá)式
component.template = container.innerHTML
}
// 清空掛載點
// clear content before mounting
container.innerHTML = ''
// 3. 調(diào)用 mount 真正的掛載
// 調(diào)用從應(yīng)用實例上備份的mount 進(jìn)行掛載
const proxy = mount(container, false, container instanceof SVGElement)
// 掛載完成后, 清理v-clock指令, 為容器添加data-v-app 標(biāo)識
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
// 返回根組件實例對象(注意不是app應(yīng)用實例對象)
return proxy
}
// 返回應(yīng)用對象
return app
}) as CreateAppFunction<Element>
可以看到重寫的mount
方法通過調(diào)用normalizeContainer
獲取掛載點容器, 其具體實現(xiàn)如下:
源碼:
// 獲取掛載點dom 容器
function normalizeContainer(
container: Element | ShadowRoot | string
): Element | null {
// 1. 參數(shù)為字符串, 則通過querySelector 獲取dom 元素
if (isString(container)) {
const res = document.querySelector(container)
return res
}
// 2. 參數(shù)不是字符串, 則直接返回
return container as any
}
在調(diào)用mount
方法掛載時, 參數(shù)可以是字符串, 也可以是真實的DOM元素.因此在該方法中針對參數(shù)進(jìn)行判斷,
- 如果參數(shù)是字符串, 則認(rèn)為是選擇器, 通過
querySelector
獲取DOM元素 - 如果參數(shù)不是字符串, 則認(rèn)為參數(shù)是真實的DOM 元素, 直接返回
上面重寫的mount
方法, 其實最主要的就是做了三件事
- 獲取掛載點容器, 通過
normalizeContainer
方法獲取 - 處理根組件對象, 根組件對象如果不具有渲染函數(shù)
render
方法和模板template
, 則使用掛載點元素內(nèi)容作為template
模板 - 調(diào)用應(yīng)用對象
app
原本的mount
方法掛載根組件, 掛載完成后為容器添加特定的屬性
3.3 應(yīng)用對象本身的mount 方法
在重寫mount
方法中最終會調(diào)用應(yīng)用對象app
本身的mount
方法進(jìn)行正式的掛載,
mount
方法接收三個參數(shù)
- 掛載點容器:
container
- 是否為
SSR
: 傳入固定值false
- 是否為
SVG
元素
其mount
方法具體實現(xiàn)如下
let isMounted = false
const app = {
//...
// app 掛載方法
mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean) {
// 判斷是否已經(jīng)掛載
if (!isMounted) {
// #5571
// 如果掛載容器具有__vue_app__, 表示當(dāng)前容器已就作為其他應(yīng)用掛載容器
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 1. 調(diào)用createVNode 創(chuàng)建根組件的VNode
// rootComponent, rootProps 是createApp 調(diào)用時傳入的參數(shù), 根組件對象與props
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 在根VNode上綁定應(yīng)用上下文對象,在掛載完畢后綁定到根組件實例對象上
vnode.appContext = context
// HMR root reload
// 熱更新
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
// 2. 渲染 VNode
// 其他渲染方式
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 瀏覽器調(diào)用render 渲染根VNode
// render 函數(shù)在創(chuàng)建創(chuàng)建渲染函數(shù)中定義, 傳遞到createAppAPI, 通過閉包緩存
render(vnode, rootContainer, isSVG)
}
// 掛載完畢后, isMounted 狀態(tài)設(shè)置為true
isMounted = true
// 應(yīng)用實例上綁定掛載容器
app._container = rootContainer
// for devtools and telemetry
// 將app 應(yīng)用實例對象綁定到容器上, 因此可以通過容器訪問app 實例
;(rootContainer as any).__vue_app__ = app
// 開發(fā)環(huán)境調(diào)試
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || 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)\``
)
}
},
}
通過上面的分析, 在掛載應(yīng)用實例時, 去除邊緣判斷,核心邏輯就以下兩點
- 通過調(diào)用
createVNode
創(chuàng)建虛擬節(jié)點, 參數(shù)為根組件對象和向根組件傳入的props
對象 - 通過調(diào)用
rendder
方法渲染虛擬節(jié)點
接下來我們分別分析創(chuàng)建虛擬節(jié)點和渲染虛擬節(jié)點邏輯
3.4 createVNode 創(chuàng)建VNode 函數(shù)
虛擬節(jié)點的本質(zhì)就是一個JS
對象, 通過屬性描述一個DOM
節(jié)點, 包括tag
, props
,children
等
具體看一下createVNode
函數(shù)的實現(xiàn)
// 創(chuàng)建createVNode 函數(shù)
export const createVNode = ( __DEV__ ? createVNodeWithArgsTransform : _createVNode )
// 創(chuàng)建具有參數(shù)轉(zhuǎn)換的VNode 函數(shù)
const createVNodeWithArgsTransform = (...args) => {
// 調(diào)用_createVNode 創(chuàng)建VNode
return _createVNode(
...(vnodeArgsTransformer
? vnodeArgsTransformer(args, currentRenderingInstance)
: args)
)
}
通過代碼不難看出, 真正來創(chuàng)建虛擬節(jié)點的函數(shù)是_createVNode
.
開發(fā)環(huán)境調(diào)用createVNodeWithArgsTransform
也只不過根據(jù)vnodeArgsTransformer
函數(shù)是否存在來轉(zhuǎn)換一下參數(shù), 最終函數(shù)返回_createVNode
調(diào)用的結(jié)果
因此我們將通過_createVNode
函數(shù)分析創(chuàng)建的虛擬節(jié)點(vnode)
_createVNode
的具體實現(xiàn)
// _createVNode 第一個參數(shù)是必傳的, 后面參數(shù)都具有默認(rèn)值
function _createVNode(
type, // 創(chuàng)建VNode的類型
props = null,
children = null,
patchFlag = 0,
dynamicProps = null,
isBlockNode = false
) {
// type 不存在 或 type 值為 Symbol(v-ndc)
// 則提示type 無效, 并將type 認(rèn)定為 Comment 注釋類型
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 判斷type 是否已經(jīng)是一個VNode
if (isVNode(type)) {
//...
}
// 調(diào)用isClassComponent 判斷是否為有狀態(tài)的組件
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// props 參數(shù)存在, 則規(guī)范處理class, style
// class & style normalization.
if (props) {
//...
}
// 判斷VNode 標(biāo)識, 采用二進(jìn)制表示, 示例代碼標(biāo)識為 100 = 4
// encode the vnode type information into a bitmap
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
// 判斷節(jié)點狀態(tài)
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
//...
}
// 返回createBaseVNode 創(chuàng)建的VNode
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
從源碼中可以看出,_createVNode
函數(shù)只是對需要創(chuàng)建虛擬節(jié)點的參數(shù)進(jìn)行各種判斷檢查, 規(guī)范props
數(shù)據(jù), 以及創(chuàng)建VNode
參數(shù)的節(jié)點標(biāo)識
最后返回createBaseVNode
函數(shù)創(chuàng)建的節(jié)點
createBaseVNode
函數(shù)的實現(xiàn)如下:
// 創(chuàng)建VNode
function createBaseVNode(
type,
props = null,
children = null,
patchFlag = 0,
dynamicProps = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
// 虛擬節(jié)點對象 VNode
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,
ctx: currentRenderingInstance
} as VNode
// ...
return vnode
}
可以看出createBaseVNode
函數(shù)最大的作用就是創(chuàng)建VNode
, 并返回該虛擬節(jié)點, 即JavaScript
對象
3.5 render 渲染VNode
在創(chuàng)建完虛擬DOM 后, 就會調(diào)用render
方法渲染虛擬節(jié)點
render
函數(shù)是在講createRenderer
的時候出現(xiàn)的萤捆,是在baseCreateRenderer
中定義的
具體源碼如下
function baseCreateRenderer(options,createHydrationFns) {
//...
// render 渲染VNode 函數(shù)
const render = (vnode, container, isSVG) => {
// 如果VNode 不存在, 有可能之前已經(jīng)掛載, 那么將會執(zhí)行卸載操作
if (vnode == null) {
// container._vnode 表示上一次渲染的VNode, 存在則會執(zhí)行卸載操作
if (container._vnode) {
unmount(container._vnode, null, null, true // 卸載VNode
}
} else {
// 如果VNode 存在, 則調(diào)用patch 方法, 判斷處理除此渲染或更新渲染
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
// 刷新任務(wù)隊列, 暫不分析
flushPreFlushCbs()
flushPostFlushCbs()
// 渲染完畢后, 容器記錄渲染的VNode
container._vnode = vnode
}
//...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
render
渲染函數(shù)的主要作用就是分發(fā)
- 渲染的
vnode
為空, 則判斷之前已渲染的情況下,則調(diào)用unmount
進(jìn)行卸載操作 - 渲染的
vnode
不為空, 則調(diào)用patch
函數(shù), 根據(jù)情況執(zhí)行初始渲染或更新渲染操作
4. 初始化應(yīng)用總結(jié)
分析到這里,以上的內(nèi)容就是createApp
源碼的基本內(nèi)容俗批。主要作用就是確定渲染器俗或,創(chuàng)建一個Vue
應(yīng)用實例,最后進(jìn)行掛載岁忘,掛載之后具體渲染邏輯,后面分析
接下來對本節(jié)初始化應(yīng)用進(jìn)行總結(jié),主要有兩部分內(nèi)容: 創(chuàng)建應(yīng)用實例對象
和 掛載應(yīng)用
4.1 創(chuàng)建應(yīng)用實例對象
創(chuàng)建應(yīng)用實例對象步驟:
- 調(diào)用
createApp
函數(shù)創(chuàng)建應(yīng)用實例 - 在
createApp
函數(shù)中通過createRenderer
函數(shù)創(chuàng)建渲染器 -
createRenderer
函數(shù)返回渲染器對象(renderer)
:{render, createApp }
- 調(diào)用渲染器中的
createApp
方法創(chuàng)建應(yīng)用實例對象 -
createApp
函數(shù)中重寫mount
掛載方法,并返回應(yīng)用實例對象
4.2 掛載應(yīng)用
調(diào)用應(yīng)用實例對象的mount
方法進(jìn)行掛載
- 首先在重寫的
mount
方法中處理獲取掛載容器的DOM元素, 以及渲染根組件的模板 - 其后調(diào)用應(yīng)用實例對象本身的
mount
方法 - 在
mount
掛載方法中根據(jù)根組件調(diào)用createVNode
函數(shù)創(chuàng)建vnode
- 之后在調(diào)用渲染器中的
render
渲染函數(shù), 渲染根組件生成的vnode
簡單來說初始化應(yīng)用就是做了如下幾件事
- 創(chuàng)建
Vue
應(yīng)用實例對象辛慰。 - 確定應(yīng)用掛載容器節(jié)點。
- 創(chuàng)建根組件
vnode
對象 - 執(zhí)行
render
渲染根組件vnode
干像。
至此, 整個createApp初始化應(yīng)用我就帶大家分析完了, 你可以回頭去前言在看一眼createApp應(yīng)用的流程圖.
最后: 如果覺得這篇文章對你有幫助, 點贊關(guān)注不迷路, 你的支持是我的動力!
[圖片上傳失敗...(image-45b9f3-1715600214328)]