前言:
在源碼分析概述中, 我們對(duì)源碼源碼結(jié)構(gòu)進(jìn)行了分析, 整個(gè)vue三大核心系統(tǒng), 即響應(yīng)式系統(tǒng), 渲染系統(tǒng),編譯系統(tǒng). 而響應(yīng)式系統(tǒng)和渲染系統(tǒng) 又共同構(gòu)建了vue官網(wǎng)上所謂的運(yùn)行時(shí)構(gòu)建
vue3 源碼中的三大系統(tǒng)有明確的分界, 但在使用時(shí), 通常并不會(huì)單獨(dú)區(qū)分使用某一個(gè)系統(tǒng), vue3的應(yīng)用是在三個(gè)系統(tǒng)相互作用的結(jié)果, 至少是運(yùn)行時(shí), 即包括渲染系統(tǒng) 和響應(yīng)式系統(tǒng).
所以我的對(duì)vue3源碼分析會(huì)根據(jù)需要交替分析三大系統(tǒng)中對(duì)應(yīng)的源碼
在vue3中, 我們需要使用createApp API 來創(chuàng)建應(yīng)用實(shí)例, 然后通過創(chuàng)建的實(shí)例調(diào)用mount方法將應(yīng)用掛載到DOM 節(jié)點(diǎn)上, 因此createApp可以理解為整個(gè)vue3 應(yīng)用的入口, 包括官網(wǎng)API都將createApp排列在第一位
所以今天這篇文章, 主要針對(duì)createApp創(chuàng)建vue3應(yīng)用的API 進(jìn)行源碼分析.
在分析之前, 我先放一張createApp初始化應(yīng)用的流程圖:
createApp初始化流程圖如下:
不知道為什么簡(jiǎn)書一直傳不上去圖片, 如果大家想看流程圖, 可以去我的
CSDN博客中看,地址給到大家
CSDN博客
如果你此時(shí)看到該流程圖是一個(gè)呆萌的狀態(tài), 可以在看完該文章后在回頭來看一下該流程圖, 你就會(huì)非常清楚的知道createApp 函數(shù)都做了什么
好, 接下來我們開始正式分析createApp創(chuàng)建應(yīng)用API的具體實(shí)現(xiàn), 那接下請(qǐng)大家跟著我一起探尋一下createApp API 源碼內(nèi)部邏輯, 看看源碼中都做了些什么.
1. 初始化應(yīng)用
我們使用一個(gè)具體的實(shí)例作為切入點(diǎn), 通過斷點(diǎn)調(diào)試的方式對(duì)createApp源碼進(jìn)行分析.理解vue3初始化的過程.
實(shí)例如下:
const { createApp } = Vue
const app = createApp({
template: '<h1>hello</h1>'
})
app.mount('#app')
這是一個(gè)最簡(jiǎn)單的實(shí)例, 在實(shí)例中, 我們首先通過解構(gòu)的方式獲取createApp方法.
通過createApp方法創(chuàng)建應(yīng)用, 并通過mount方法將應(yīng)用掛載到DOM 上
接下來我們將通過這個(gè)實(shí)例分析createApp創(chuàng)建vue3應(yīng)用的API , 以及mount掛載方法.
2. 創(chuàng)建vue3應(yīng)用的源碼分析
2.1 定位 createApp 方法
我們知道vue3 源碼采用pnpm多包管理, 目前我們只知道createAppAPI 是從vue中結(jié)構(gòu)出來的, 那么該API具體來自于哪個(gè)包中, 現(xiàn)在并不知道.
因此在分析createAppAPI 前, 我們需要先確認(rèn)該API 來自于哪個(gè)包.
我們可以通過斷點(diǎn)調(diào)試的分時(shí)來分析, 使用單步調(diào)試, 進(jìn)入到createApp方法內(nèi)
此時(shí)就可以看到createApp方法來自于packages/runtime-dom包中
runtime是運(yùn)行時(shí).其中包括runtime-dom,runtime-core兩個(gè)包
-
runtime-dom處理與瀏覽器相關(guān)的dom 操作的API -
runtime-core是運(yùn)行時(shí)核心代碼,與渲染平臺(tái)無關(guān)
也就意味著runtime-dom的運(yùn)行依賴于runtime-core包, 所以在runtime-dom/src/index.ts模塊中一定會(huì)引入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ù)后, 接下來就是分析該方法的實(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>
這里我對(duì)源碼進(jìn)行了精簡(jiǎn), 這樣非常方便的可以看出.createApp函數(shù)的核心邏輯就是創(chuàng)建Vue3應(yīng)用對(duì)象app并返回, 至于重寫mount方法, 稍后我會(huì)帶大家具體分析
我們先看下createApp的入?yún)? 用過createAppAPI的朋友, 相信大家都知道, createApp接收兩個(gè)入?yún)?/p>
-
根組件: 根組件可以是單文件組件, 也可以是組件的選項(xiàng)對(duì)象 -
props: 向根組件傳入的props數(shù)據(jù), 該參數(shù)為可選參數(shù).
在源碼中可以看出, createApp函數(shù)通過...args剩余運(yùn)算符收集所有入?yún)? 將參數(shù)組合成為數(shù)組. 如果傳入了第二個(gè)參數(shù)props, args數(shù)組收集到的數(shù)據(jù)將會(huì)有兩項(xiàng), 否則將只有一項(xiàng), 即根組件對(duì)象.
所以createApp函數(shù)會(huì)接收根組件作為參數(shù), 并且返回vue應(yīng)用, 即方法內(nèi)app就是createApp創(chuàng)建的應(yīng)用對(duì)象
我相信大家已經(jīng)看出來了, createApp函數(shù)中創(chuàng)建的應(yīng)用對(duì)象app其本質(zhì)是通過ensureRenderer().createApp(...args)創(chuàng)建的
通過這個(gè)結(jié)構(gòu)不難看出: ensureRenderer()方法調(diào)用完畢后返回一個(gè)包含createApp方法的對(duì)象, 并通過該方法的調(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ù)的作用就是用來確定渲染器對(duì)象(renderer)存在.
如果渲染器renderer不存在, 就會(huì)調(diào)用createRenderer函數(shù)創(chuàng)建渲染器, 根據(jù)前面的分析, 渲染器將是一個(gè)包含createApp方法的對(duì)象
初始創(chuàng)建vue應(yīng)用renderer值為必然為空, 會(huì)調(diào)用createRenderer函數(shù), 并傳入?yún)?shù)rendererOptions
這里rendererOptions參數(shù)是渲染器配置對(duì)象, 主要作用就是用來操作DOM的一些方法
源碼:
// 渲染器配置對(duì)象
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
// extend 就是合并對(duì)象的方法(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é)點(diǎn)
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
// 創(chuàng)建文本節(jié)點(diǎn)
createText: text => doc.createTextNode(text),
// 創(chuàng)建注釋節(jié)點(diǎn)
createComment: text => doc.createComment(text),
//...
}
這里將幾個(gè)模塊中的中的方法放在了一起, 很方便的看出, rendererOptions渲染器配置對(duì)象就是封裝了DOM操作方法的對(duì)象, 由兩部分組成:
-
patchProp:處理元素的props、Attribute、class、style、event事件等。 -
nodeOps:處理DOM節(jié)點(diǎn),這個(gè)對(duì)象里面包含了各種封裝原生DOM操作的方法。
rendererOptions對(duì)象最終會(huì)傳遞到渲染器里面,里面的各種方法最終會(huì)在頁面渲染的過程中被調(diào)用。
至此我們暫時(shí)了解如下幾件事:
-
createApp方法的作用就是創(chuàng)建Vue3應(yīng)用. -
createApp方法內(nèi)真正創(chuàng)建Vue3應(yīng)用的是渲染器對(duì)象的createApp方法 -
createRenderer方法的作用是用來創(chuàng)建渲染器對(duì)象, 接收rendererOptions渲染器配置對(duì)象 -
rendererOptions配置對(duì)象中包含DOM操作的方法
因此要?jiǎng)?chuàng)建Vue3 應(yīng)用必須先創(chuàng)建渲染器對(duì)象, 因此接下來我們繼續(xù)分析createRenderer方法的實(shí)現(xiàn)
2.4 createRenderer 創(chuàng)建渲染器方法
createRenderer方法來源于runtime-core/src/render.ts模塊
具體源碼實(shí)現(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)建渲染器對(duì)象的函數(shù)是baseCreateRenderer
baseCreateRenderer創(chuàng)建的渲染器對(duì)象包括render, hydrate, createApp三個(gè)方法
獲取到渲染器對(duì)象后, 就會(huì)通過渲染器對(duì)象調(diào)用createApp方法創(chuàng)建應(yīng)用. 而createApp方法的值是通過createAppAPI方法創(chuàng)建的, 同時(shí)會(huì)將render, hydrate,兩個(gè)方法作為參數(shù)傳入createAppAPI方法中.
所以接下來我們需要看一下createAppAPI方法的實(shí)現(xiàn)
2.5 createApp API 方法
createAppAPI方法來自于runtime-core/src/apiCreateApp.ts
具體實(shí)現(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ù)必須是一個(gè)對(duì)象或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)用實(shí)例上下文對(duì)象, 就是一些默認(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)用實(shí)例對(duì)象
// 同時(shí)將應(yīng)用實(shí)例存儲(chǔ)到context上下文的app屬性
const app: App = (context.app = {
// 初始化應(yīng)用屬性
_uid: uid++, // 項(xiàng)目中可能存在多個(gè)vue實(shí)例,需使用id標(biāo)識(shí)
_component: rootComponent as ConcreteComponent, // 根組件
_props: rootProps, // 傳遞給根組件的props
_container: null, // 掛載點(diǎn): DOM容器
_context: context, // app上下文
_instance: null, // 應(yīng)用實(shí)例
version, // 版本
// 定義了一個(gè)訪問器屬性app.config,只能讀取,不能直接替換
get config() {return context.config},
set config(v) {},
// 掛載插件, 返回app對(duì)象
use(plugin, ...options) {/*... 省略代碼*/ return app },
// 混入
mixin(mixin) { /*... 省略代碼*/ return app },
// 注冊(cè)全局組件
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 對(duì)象
return app
}
}
源碼中createAppAPI函數(shù)返回了一個(gè)函數(shù), 返回的這個(gè)函數(shù)才是真正的createApp函數(shù).
因此該返回的函數(shù)就是最后用來創(chuàng)建應(yīng)用對(duì)象的createApp方法
分析至此,我們就可以知道了
-
createAppAPI 創(chuàng)建應(yīng)用實(shí)例對(duì)象并返回 -
createAppAPI 中創(chuàng)建應(yīng)用對(duì)象, 是通過調(diào)用渲染器{render,hydrate,createApp}中的createApp方法創(chuàng)建的 - 渲染器中的
createApp方法則是createAppAPI函數(shù)的返回函數(shù)
而createApp創(chuàng)建的應(yīng)用對(duì)象就是包含了一些屬性和常用的use,mixins,component,directive,mount,unmount,provide方法的對(duì)象. 因此可以通過app應(yīng)用對(duì)象調(diào)用mount掛載應(yīng)用
3. mount 掛載源碼分析
在上面我們已經(jīng)分析了createApp函數(shù), 在調(diào)用完畢后返回vue應(yīng)用實(shí)例對(duì)象, 在使用時(shí)會(huì)通過應(yīng)用實(shí)例對(duì)象調(diào)用mount方法掛載應(yīng)用.
3.1 確定mount 方法
通過前面的分析,我們已經(jīng)明白, 創(chuàng)建應(yīng)用實(shí)例的createApp函數(shù)是通過調(diào)用createApp API方法返回的createApp 創(chuàng)建的應(yīng)用實(shí)例. 在此方法中可以看到生成應(yīng)用對(duì)象app具有mount方法
源碼:
export function createAppAPI(render,hydrate){
// 創(chuàng)建vue應(yīng)用實(shí)例的方法
return function createApp(rootComponent, rootProps = null) {
//...
// 應(yīng)用實(shí)例對(duì)象
const app: App = (context.app = {
//..
// 應(yīng)用實(shí)例對(duì)象掛載mount 方法
mount(rootContainer,isHydrate,isSVG) {
// ...
},
//...
})
//..
// 返回應(yīng)用實(shí)例對(duì)象
return app
}
}
但我們調(diào)用app.mount()掛載應(yīng)用時(shí)并不是調(diào)用該mount 方法, 原因在于createApp方法中對(duì)當(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)用實(shí)例對(duì)象, 接著通過解構(gòu)的方式將應(yīng)用實(shí)例對(duì)象app的mount方法記錄在變量mount上
然后重寫app對(duì)象的mount方法進(jìn)行重寫, 賦值了一個(gè)新的方法
也就是在在我們通過app.mount()掛載應(yīng)用時(shí), 進(jìn)入的是createApp函數(shù)中對(duì)app應(yīng)用對(duì)象重寫的mount方法
3.2 mount 掛載
在實(shí)例中,我們調(diào)用mount方法進(jìn)行掛載時(shí),傳入了一個(gè)字符串#app , 以此獲取到掛載點(diǎn)DOM元素
app.mount('#app')
mount具體是如何掛載的呢?, 我們首先分析一下createApp函數(shù)中重寫的mount方法
重寫的mount方法
export const createApp = ((...args) => {
// 創(chuàng)建應(yīng)用實(shí)例對(duì)象
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)用對(duì)象獲取根組件, 即createApp() 方法第一個(gè)參數(shù)
const component = app._component
// 驗(yàn)證根組件是一個(gè)對(duì)象,且不具有render, template屬性, 則使用掛載點(diǎn)內(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.
// 需要確保掛載點(diǎn)模板是可信的, 因?yàn)槟0逯锌赡艽嬖贘S表達(dá)式
component.template = container.innerHTML
}
// 清空掛載點(diǎn)
// clear content before mounting
container.innerHTML = ''
// 3. 調(diào)用 mount 真正的掛載
// 調(diào)用從應(yīng)用實(shí)例上備份的mount 進(jìn)行掛載
const proxy = mount(container, false, container instanceof SVGElement)
// 掛載完成后, 清理v-clock指令, 為容器添加data-v-app 標(biāo)識(shí)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
// 返回根組件實(shí)例對(duì)象(注意不是app應(yīng)用實(shí)例對(duì)象)
return proxy
}
// 返回應(yīng)用對(duì)象
return app
}) as CreateAppFunction<Element>
可以看到重寫的mount方法通過調(diào)用normalizeContainer獲取掛載點(diǎn)容器, 其具體實(shí)現(xiàn)如下:
源碼:
// 獲取掛載點(diǎ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í), 參數(shù)可以是字符串, 也可以是真實(shí)的DOM元素.因此在該方法中針對(duì)參數(shù)進(jìn)行判斷,
- 如果參數(shù)是字符串, 則認(rèn)為是選擇器, 通過
querySelector獲取DOM元素 - 如果參數(shù)不是字符串, 則認(rèn)為參數(shù)是真實(shí)的DOM 元素, 直接返回
上面重寫的mount方法, 其實(shí)最主要的就是做了三件事
- 獲取掛載點(diǎn)容器, 通過
normalizeContainer方法獲取 - 處理根組件對(duì)象, 根組件對(duì)象如果不具有渲染函數(shù)
render方法和模板template, 則使用掛載點(diǎn)元素內(nèi)容作為template模板 - 調(diào)用應(yīng)用對(duì)象
app原本的mount方法掛載根組件, 掛載完成后為容器添加特定的屬性
3.3 應(yīng)用對(duì)象本身的mount 方法
在重寫mount方法中最終會(huì)調(diào)用應(yīng)用對(duì)象app本身的mount方法進(jìn)行正式的掛載,
mount方法接收三個(gè)參數(shù)
- 掛載點(diǎn)容器:
container - 是否為
SSR: 傳入固定值false - 是否為
SVG元素
其mount方法具體實(shí)現(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í)傳入的參數(shù), 根組件對(duì)象與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)用上下文對(duì)象,在掛載完畢后綁定到根組件實(shí)例對(duì)象上
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)用實(shí)例上綁定掛載容器
app._container = rootContainer
// for devtools and telemetry
// 將app 應(yīng)用實(shí)例對(duì)象綁定到容器上, 因此可以通過容器訪問app 實(shí)例
;(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)用實(shí)例時(shí), 去除邊緣判斷,核心邏輯就以下兩點(diǎn)
- 通過調(diào)用
createVNode創(chuàng)建虛擬節(jié)點(diǎn), 參數(shù)為根組件對(duì)象和向根組件傳入的props對(duì)象 - 通過調(diào)用
rendder方法渲染虛擬節(jié)點(diǎn)
接下來我們分別分析創(chuàng)建虛擬節(jié)點(diǎn)和渲染虛擬節(jié)點(diǎn)邏輯
3.4 createVNode 創(chuàng)建VNode 函數(shù)
虛擬節(jié)點(diǎn)的本質(zhì)就是一個(gè)JS對(duì)象, 通過屬性描述一個(gè)DOM 節(jié)點(diǎn), 包括tag, props,children等
具體看一下createVNode函數(shù)的實(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é)點(diǎn)的函數(shù)是_createVNode.
開發(fā)環(huán)境調(diào)用createVNodeWithArgsTransform也只不過根據(jù)vnodeArgsTransformer函數(shù)是否存在來轉(zhuǎn)換一下參數(shù), 最終函數(shù)返回_createVNode調(diào)用的結(jié)果
因此我們將通過_createVNode函數(shù)分析創(chuàng)建的虛擬節(jié)點(diǎn)(vnode)
_createVNode的具體實(shí)現(xiàn)
// _createVNode 第一個(gè)參數(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)是一個(gè)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)識(shí), 采用二進(jìn)制表示, 示例代碼標(biāo)識(shí)為 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é)點(diǎn)狀態(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ù)只是對(duì)需要?jiǎng)?chuàng)建虛擬節(jié)點(diǎn)的參數(shù)進(jìn)行各種判斷檢查, 規(guī)范props數(shù)據(jù), 以及創(chuàng)建VNode 參數(shù)的節(jié)點(diǎn)標(biāo)識(shí)
最后返回createBaseVNode函數(shù)創(chuàng)建的節(jié)點(diǎn)
createBaseVNode函數(shù)的實(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é)點(diǎn)對(duì)象 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é)點(diǎn), 即JavaScript 對(duì)象
3.5 render 渲染VNode
在創(chuàng)建完虛擬DOM 后, 就會(huì)調(diào)用render方法渲染虛擬節(jié)點(diǎn)
render函數(shù)是在講createRenderer的時(shí)候出現(xiàn)的,是在baseCreateRenderer中定義的
具體源碼如下
function baseCreateRenderer(options,createHydrationFns) {
//...
// render 渲染VNode 函數(shù)
const render = (vnode, container, isSVG) => {
// 如果VNode 不存在, 有可能之前已經(jīng)掛載, 那么將會(huì)執(zhí)行卸載操作
if (vnode == null) {
// container._vnode 表示上一次渲染的VNode, 存在則會(huì)執(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ù)隊(duì)列, 暫不分析
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)建一個(gè)Vue應(yīng)用實(shí)例,最后進(jìn)行掛載,掛載之后具體渲染邏輯,后面分析
接下來對(duì)本節(jié)初始化應(yīng)用進(jìn)行總結(jié),主要有兩部分內(nèi)容: 創(chuàng)建應(yīng)用實(shí)例對(duì)象 和 掛載應(yīng)用
4.1 創(chuàng)建應(yīng)用實(shí)例對(duì)象
創(chuàng)建應(yīng)用實(shí)例對(duì)象步驟:
- 調(diào)用
createApp函數(shù)創(chuàng)建應(yīng)用實(shí)例 - 在
createApp函數(shù)中通過createRenderer函數(shù)創(chuàng)建渲染器 -
createRenderer函數(shù)返回渲染器對(duì)象(renderer):{render, createApp } - 調(diào)用渲染器中的
createApp方法創(chuàng)建應(yīng)用實(shí)例對(duì)象 -
createApp函數(shù)中重寫mount掛載方法,并返回應(yīng)用實(shí)例對(duì)象
4.2 掛載應(yīng)用
調(diào)用應(yīng)用實(shí)例對(duì)象的mount方法進(jìn)行掛載
- 首先在重寫的
mount方法中處理獲取掛載容器的DOM元素, 以及渲染根組件的模板 - 其后調(diào)用應(yīng)用實(shí)例對(duì)象本身的
mount方法 - 在
mount掛載方法中根據(jù)根組件調(diào)用createVNode函數(shù)創(chuàng)建vnode - 之后在調(diào)用渲染器中的
render渲染函數(shù), 渲染根組件生成的vnode
簡(jiǎn)單來說初始化應(yīng)用就是做了如下幾件事
- 創(chuàng)建
Vue應(yīng)用實(shí)例對(duì)象。 - 確定應(yīng)用掛載容器節(jié)點(diǎn)。
- 創(chuàng)建根組件
vnode對(duì)象 - 執(zhí)行
render渲染根組件vnode。
至此, 整個(gè)createApp初始化應(yīng)用我就帶大家分析完了, 你可以回頭去前言在看一眼createApp應(yīng)用的流程圖.
最后: 如果覺得這篇文章對(duì)你有幫助, 點(diǎn)贊關(guān)注不迷路, 你的支持是我的動(dòng)力!
[圖片上傳失敗...(image-45b9f3-1715600214328)]