【vue3源碼】十一叽躯、初始vue3中的渲染器
在介紹渲染器之前。我們先簡單了解下渲染器的作用是什么肌括。
渲染器最主要的任務(wù)就是將虛擬DOM渲染成真實的DOM對象到對應(yīng)的平臺上点骑,這里的平臺可以是瀏覽器DOM平臺,也可以是其他諸如canvas的一些平臺谍夭『诘危總之vue3的渲染器提供了跨平臺的能力。
渲染器的生成
當(dāng)使用createApp
創(chuàng)建應(yīng)用實例時紧索,會首先調(diào)用一個ensureRenderer
方法袁辈。
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// ...
return app
}) as CreateAppFunction<Element>
ensureRenderer
函數(shù)會返回一個渲染器renderer
,這個renderer
是個全局變量齐板,如果不存在吵瞻,會使用createRenderer
方法進行創(chuàng)建,并將創(chuàng)建好的renderer
賦值給這個全局變量甘磨。
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
createRenderer
函數(shù)接收一個options
參數(shù)橡羞,至于這個options
中是什么,這里我們暫且先不深究济舆。createRenderer
函數(shù)中會調(diào)用baseCreateRenderer
函數(shù)卿泽,并返回其結(jié)果。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
至此滋觉,我們就找到了真正創(chuàng)建渲染器的方法baseCreateRenderer
签夭。當(dāng)我們找到baseCreateRenderer
的具體實現(xiàn),你會發(fā)現(xiàn)這個函數(shù)是十分長的椎侠,單baseCreateRenderer
這一個函數(shù)就占據(jù)了2044行代碼第租,其中更是聲明了30+個函數(shù)。
在此我們先不用關(guān)心這些函數(shù)的作用我纪,在后續(xù)介紹組件加載及更新過程時慎宾,你會慢慢了解這些函數(shù)丐吓。
接下來我們繼續(xù)看渲染器對象的結(jié)構(gòu)。
渲染器
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
在baseCreateRenderer
最后返回了一個對象趟据,這個對象包含了三個屬性:render
(渲染函數(shù))券犁、hydrate
(同構(gòu)渲染)、createApp
汹碱。這里的createApp
是不是很熟悉粘衬,在createApp
中調(diào)用ensureRenderer
方法后面會緊跟著調(diào)用了createApp
函數(shù):
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// ...
return app
}) as CreateAppFunction<Element>
注意這里不要兩個createApp
混淆了。渲染器中的createApp
并不是我們平時使用到的createApp
咳促。當(dāng)我們調(diào)用createApp
方法進行創(chuàng)建實例時稚新,會調(diào)用渲染器中的createApp
生成app
實例。
接下來我們來看下渲染器中的createApp
等缀。首先createApp
方法通過一個createAppAPI
方法生成枷莉,這個方法接收渲染器中的render
及hydrate
:
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// ...
}
}
createAppAPI
函數(shù)會返回一個閉包函數(shù)createApp
。這個createApp
就是通過ensureRenderer().createApp(...args)
調(diào)用的方法了尺迂。接下來看createApp
的具體實現(xiàn):
<details>
<summary><code>createApp</code>完整代碼</summary>
function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : '')
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
},
component(name: string, component?: Component): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// #5571
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.`
)
}
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, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
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)\``
)
}
},
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key as string | symbol] = value
return app
}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
</details>
這個createApp
函數(shù)與vue
提供的createApp
一樣笤妙,都接受一個根組件參數(shù)和一個rootProps
(根組件的props
)參數(shù)诵盼。
首先惜浅,如果根組件不是方法時俄讹,會將rootComponent
使用解構(gòu)的方式重新賦值為一個新的對象整袁,然后判斷rootProps
如果不為null
并且也不是個對象堡僻,則會將rootProps
置為null
嫉晶。
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
然后調(diào)用createAppContext()
方法創(chuàng)建一個上下文對象天吓。
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
// 一個判斷是否為原生標(biāo)簽的函數(shù)
isNativeTag: NO,
performance: false,
globalProperties: {},
// 自定義options的合并策略
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
// 組件模板的運行時編譯器選項
compilerOptions: {}
},
// 存儲全局混入的mixin
mixins: [],
// 保存全局注冊的組件
components: {},
// 保存注冊的全局指令
directives: {},
// 保存全局provide的值
provides: Object.create(null),
// 緩存組件被解析過的options(合并了全局mixins蘸际、extends祭陷、局部mixins)
optionsCache: new WeakMap(),
// 緩存每個組件經(jīng)過標(biāo)準(zhǔn)化的的props options
propsCache: new WeakMap(),
// 緩存每個組件經(jīng)過標(biāo)準(zhǔn)化的的emits options
emitsCache: new WeakMap()
}
}
然后聲明了一個installedPlugins
集合和一個布爾類型的isMounted
苍凛。其中installedPlugins
會用來存儲使用use
安裝的plugin
,isMounted
代表根組件是否已經(jīng)掛載兵志。
const installedPlugins = new Set()
let isMounted = false
緊接著聲明了一個app
變量醇蝴,這個app
變量就是app
實例,在創(chuàng)建app
的同時會將app
添加到上下文中的app
屬性中想罕。
const app: APP = (context.app = { //... })
然后會處理vue2
的兼容悠栓。這里我們暫時不深究vue2
的兼容處理。
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
最后返回app
按价。至此createaApp
執(zhí)行完畢惭适。
app應(yīng)用實例
接下來我們看下app
實例的構(gòu)造:
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) { //... }
})
-
_uid
:app
的唯一標(biāo)識,每次都會使用uid
為新app
的唯一標(biāo)識楼镐,在賦值后癞志,uid
會進行自增,以便下一個app
使用 -
_component
:根組件 -
_props
:根組件所需的props
-
_container
:需要將根組件渲染到的容器 -
_context
:app
的上下文 -
_instance
:根組件的實例 -
version
:vue
的版本 -
get config
:獲取上下文中的config
get config() { return context.config }
-
set config
:攔截app.config
的set
操作框产,防止app.config
被修改set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }
app.use()
使用app.use
方法安裝plugin
今阳。對于重復(fù)安裝多次的plugin
师溅,只會進行安裝一次,這都依靠installedPlugins
盾舌,每次安裝新的plugin
后,都會將plugin
存入installedPlugins
蘸鲸,這樣如果再次安裝同樣的plugin
妖谴,就會避免多次安裝。
use(plugin: Plugin, ...options: any[]) {
// 如果已經(jīng)安裝過plugin酌摇,則不需要再次安裝
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) { // 如果存在plugin膝舅,并且plugin.install是個方法
// 將plugin添加到installedPlugins
installedPlugins.add(plugin)
// 調(diào)用plugin.install
plugin.install(app, ...options)
} else if (isFunction(plugin)) { 如果plugin是方法
// 將plugin添加到installedPlugins
installedPlugins.add(plugin)
// 調(diào)plugin
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
// 最后返回app,以便可以鏈?zhǔn)秸{(diào)用app的方法
return app
}
app.mixin()
使用app.mixin
進行全局混入窑多,被混入的對象會被存在上下文中的mixins
中仍稀。注意mixin
只會在支持options api
的版本中才能使用,在mixin
中會通過__FEATURE_OPTIONS_API__
進行判斷埂息,這個變量會在打包過程中借助@rollup/plugin-replace
進行替換技潘。
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : '')
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
}
app.component()
使用app.compoent
全局注冊組件,也可用來獲取name
對應(yīng)的組件千康。被注冊的組件會被存在上下文中的components
中享幽。
component(name: string, component?: Component): any {
// 驗證組件名是否符合要求
if (__DEV__) {
validateComponentName(name, context.config)
}
// 如果不存在component,那么會返回name對應(yīng)的組件
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
}
app.directive()
注冊全局指令拾弃,也可用來獲取name
對應(yīng)的指令對象值桩。注冊的全局指令會被存入上下文中的directives
中。
directive(name: string, directive?: Directive) {
// 驗證指令名稱
if (__DEV__) {
validateDirectiveName(name)
}
// 如果不存在directive豪椿,則返回name對應(yīng)的指令對象
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
}
app.mount()
此處的app.mount
并不是我們平時使用到的mount
奔坟。創(chuàng)建完渲染器,執(zhí)行完渲染器的createApp
后搭盾,會重寫mount
方法咳秉,我們使用的mount
方法是被重寫的mount
方法。
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 如果未掛載增蹭,開始掛載
if (!isMounted) {
// 如果存在rootContainer.__vue_app__滴某,說明容器中已經(jīng)存在一個app實例了,需要先使用unmount進行卸載
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.`
)
}
// 創(chuàng)建根組件的虛擬DOM
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 將上下文添加到根組件虛擬dom的appContext屬性中
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
// 同構(gòu)渲染
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 客戶端渲染
render(vnode, rootContainer, isSVG)
}
// 渲染完成后將isMounted置為true
isMounted = true
// 將容器添加到app的_container屬性中
app._container = rootContainer
// 將rootContainer.__vue_app__指向app實例
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// 將根組件實例賦給app._instance
app._instance = vnode.component
devtoolsInitApp(app, version)
}
// 返回根組件expose的屬性
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) { // 已經(jīng)掛載了
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)\``
)
}
}
app.unmount()
卸載應(yīng)用實例滋迈。
unmount() {
// 如果已經(jīng)掛載才能進行卸載
if (isMounted) {
// 調(diào)用redner函數(shù)霎奢,此時虛擬節(jié)點為null,代表會清空容器中的內(nèi)容
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// 將app._instance置空
app._instance = null
devtoolsUnmountApp(app)
}
// 刪除容器中的__vue_app__
delete app._container.__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
}
app.provide()
全局注入一些數(shù)據(jù)饼灿。這些數(shù)據(jù)會被存入上下文對象的provides
中幕侠。
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key as string | symbol] = value
return app
}
總結(jié)
vue3
中的渲染器主要作用就是將虛擬DOM轉(zhuǎn)為真實DOM渲染到對應(yīng)平臺中,在這個渲染過程中會包括DOM的掛載碍彭、DOM的更新等操作晤硕。
通過baseCreateRenderer
方法會創(chuàng)建一個渲染器renderer
悼潭,renderer
中有三個方法:render
、hydrate
舞箍、createApp
舰褪,其中render
方法用來進行客戶端渲染,hydrate
用來進行同構(gòu)渲染疏橄,createApp
用來創(chuàng)建app
實例占拍。baseCreateRenderer
中包含了大量的函數(shù)用來處理掛載組件、更新組件等操作捎迫。