共计 11906 个字符,预计需要花费 30 分钟才能阅读完成。
【vue3 源码】十一、初始 vue3 中的渲染器
在介绍渲染器之前。咱们先简略理解下渲染器的作用是什么。
渲染器最次要的工作就是将虚构 DOM 渲染成实在的 DOM 对象到对应的平台上,这里的平台能够是浏览器 DOM 平台,也能够是其余诸如 canvas 的一些平台。总之 vue3 的渲染器提供了跨平台的能力。
渲染器的生成
当应用 createApp
创立利用实例时,会首先调用一个 ensureRenderer
办法。
export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)
// ...
return app
}) as CreateAppFunction<Element>
ensureRenderer
函数会返回一个渲染器 renderer
,这个renderer
是个全局变量,如果不存在,会应用 createRenderer
办法进行创立,并将创立好的 renderer
赋值给这个全局变量。
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
createRenderer
函数接管一个 options
参数,至于这个 options
中是什么,这里咱们暂且先不深究。createRenderer
函数中会调用 baseCreateRenderer
函数,并返回其后果。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {return baseCreateRenderer<HostNode, HostElement>(options)
}
至此,咱们就找到了真正创立渲染器的办法 baseCreateRenderer
。当咱们找到baseCreateRenderer
的具体实现,你会发现这个函数是非常长的,单 baseCreateRenderer
这一个函数就占据了 2044 行代码,其中更是申明了 30+ 个函数。
在此咱们先不必关怀这些函数的作用,在后续介绍组件加载及更新过程时,你会缓缓理解这些函数。
接下来咱们持续看渲染器对象的构造。
渲染器
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
在 baseCreateRenderer
最初返回了一个对象,这个对象蕴含了三个属性:render
(渲染函数)、hydrate
(同构渲染)、createApp
。这里的 createApp
是不是很相熟,在 createApp
中调用 ensureRenderer
办法前面会紧跟着调用了 createApp
函数:
export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)
// ...
return app
}) as CreateAppFunction<Element>
留神这里不要两个 createApp
混同了。渲染器中的 createApp
并不是咱们平时应用到的 createApp
。当咱们调用createApp
办法进行创立实例时,会调用渲染器中的 createApp
生成 app
实例。
接下来咱们来看下渲染器中的 createApp
。首先createApp
办法通过一个 createAppAPI
办法生成,这个办法接管渲染器中的 render
及hydrate
:
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {return function createApp(rootComponent, rootProps = null) {// ...}
}
createAppAPI
函数会返回一个闭包函数 createApp
。这个createApp
就是通过 ensureRenderer().createApp(...args)
调用的办法了。接下来看 createApp
的具体实现:
<details>
<summary>createApp
残缺代码 </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
函数与 vue
提供的 createApp
一样,都承受一个根组件参数和一个rootProps
(根组件的props
)参数。
首先,如果根组件不是办法时,会将 rootComponent
应用解构的形式从新赋值为一个新的对象,而后判断 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
}
而后调用 createAppContext()
办法创立一个上下文对象。
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
// 一个判断是否为原生标签的函数
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(),
// 缓存每个组件通过标准化的的 props options
propsCache: new WeakMap(),
// 缓存每个组件通过标准化的的 emits options
emitsCache: new WeakMap()}
}
而后申明了一个 installedPlugins
汇合和一个布尔类型的 isMounted
。其中installedPlugins
会用来存储应用 use
装置的 plugin
,isMounted
代表根组件是否曾经挂载。
const installedPlugins = new Set()
let isMounted = false
紧接着申明了一个 app
变量,这个 app
变量就是 app
实例,在创立 app
的同时会将 app
增加到上下文中的 app
属性中。
const app: APP = (context.app = { //...})
而后会解决 vue2
的兼容。这里咱们临时不深究 vue2
的兼容解决。
if (__COMPAT__) {installAppCompatProperties(app, context, render)
}
最初返回 app
。至此createaApp
执行结束。
app 利用实例
接下来咱们看下 app
实例的结构:
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
的惟一标识,每次都会应用uid
为新app
的惟一标识,在赋值后,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
。对于反复装置屡次的plugin
,只会进行装置一次,这都依附installedPlugins
,每次装置新的plugin
后,都会将 plugin
存入installedPlugins
,这样如果再次装置同样的plugin
,就会防止屡次装置。
use(plugin: Plugin, ...options: any[]) {
// 如果曾经装置过 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)
// 调用 plugin.install
plugin.install(app, ...options)
} else if (isFunction(plugin)) { 如果 plugin 是办法
// 将 plugin 增加到 installedPlugins
installedPlugins.add(plugin)
// 调 plugin
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
// 最初返回 app,以便能够链式调用 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
对应的组件。被注册的组件会被存在上下文中的 components
中。
component(name: string, component?: Component): any {
// 验证组件名是否符合要求
if (__DEV__) {validateComponentName(name, context.config)
}
// 如果不存在 component,那么会返回 name 对应的组件
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
对应的指令对象。注册的全局指令会被存入上下文中的 directives
中。
directive(name: string, directive?: Directive) {
// 验证指令名称
if (__DEV__) {validateDirectiveName(name)
}
// 如果不存在 directive,则返回 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
}
app.mount()
此处的 app.mount
并不是咱们平时应用到的 mount
。创立完渲染器,执行完渲染器的createApp
后,会重写 mount
办法,咱们应用的 mount
办法是被重写的 mount
办法。
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 如果未挂载,开始挂载
if (!isMounted) {
// 如果存在 rootContainer.__vue_app__,阐明容器中曾经存在一个 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.`)
}
// 创立根组件的虚构 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)
}
}
// 同构渲染
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__) { // 曾经挂载了
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()
卸载利用实例。
unmount() {
// 如果曾经挂载能力进行卸载
if (isMounted) {
// 调用 redner 函数,此时虚构节点为 null,代表会清空容器中的内容
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()
全局注入一些数据。这些数据会被存入上下文对象的 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
}
总结
vue3
中的渲染器次要作用就是将虚构 DOM 转为实在 DOM 渲染到对应平台中,在这个渲染过程中会包含 DOM 的挂载、DOM 的更新等操作。
通过 baseCreateRenderer
办法会创立一个渲染器 renderer
,renderer
中有三个办法:render
、hydrate
、createApp
,其中 render
办法用来进行客户端渲染,hydrate
用来进行同构渲染,createApp
用来创立 app
实例。baseCreateRenderer
中蕴含了大量的函数用来解决挂载组件、更新组件等操作。