【Vue.js】892- Vue 3.0 进阶之动态组件探秘
本文是 Vue 3.0 进阶系列 的第四篇文章,在这篇文章中,阿宝哥将介绍 Vue 3 中的内置组件 —— component
,该组件的作用是渲染一个 “元组件” 为动态组件。如果你对动态组件还不了解的话也没关系,文中阿宝哥会通过具体的示例,来介绍动态组件的应用。
由于动态组件内部与组件注册之间有一定的联系,所以为了让大家能够更好地了解动态组件的内部原理,阿宝哥会先介绍组件注册的相关知识。
一、组件注册
1.1 全局注册
在 Vue 3.0 中,通过使用 app
对象的 component
方法,可以很容易地注册或检索全局组件。component
方法支持两个参数:
name:组件名称;
component:组件定义对象。
接下来,我们来看一个简单的示例:
<div id="app"><component-a></component-a><component-b></component-b><component-c></component-c>
</div>
<script>const { createApp } = Vueconst app = createApp({}); // ①app.component('component-a', { // ②template: "<p>我是组件A</p>"});app.component('component-b', {template: "<p>我是组件B</p>"});app.component('component-c', {template: "<p>我是组件C</p>"});app.mount('#app') // ③
</script>
在以上代码中,我们通过 app.component
方法注册了 3 个组件,这些组件都是全局注册的 。也就是说它们在注册之后可以用在任何新创建的组件实例的模板中。
该示例的代码比较简单,主要包含 3 个步骤:创建 App 对象、注册全局组件和应用挂载。其中创建 App
对象的细节,阿宝哥会在后续的文章中单独介绍,下面我们将重点分析其他 2 个步骤,首先我们先来分析注册全局组件的过程。
1.2 注册全局组件的过程
在以上示例中,我们使用 app
对象的 component
方法来注册全局组件:
app.component('component-a', {template: "<p>我是组件A</p>"
});
当然,除了注册全局组件之外,我们也可以注册局部组件,因为组件中也接受一个 components
的选项:
const app = Vue.createApp({components: {'component-a': ComponentA,'component-b': ComponentB}
})
需要注意的是,局部注册的组件在其子组件中是不可用的。接下来,我们来继续介绍注册全局组件的过程。对于前面的示例来说,我们使用的 app.component
方法被定义在 runtime-core/src/apiCreateApp.ts
文件中:
export function createAppAPI<HostElement>(render: RootRenderFunction,hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {return function createApp(rootComponent, rootProps = null) {const context = createAppContext()const installedPlugins = new Set()let isMounted = falseconst app: App = (context.app = {// 省略部分代码_context: context,// 注册或检索全局组件component(name: string, component?: Component): any {if (__DEV__) {validateComponentName(name, context.config)}if (!component) { // 获取name对应的组件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},})return app}
}
当所有的组件都注册成功之后,它们会被保存到 context
对象的 components
属性中,具体如下图所示:
顾名思义 context
是表示应用的上下文对象,那么该对象是如何创建的呢?其实,该对象是通过 createAppContext
函数来创建的:
const context = createAppContext()
而 createAppContext
函数被定义在 runtime-core/src/apiCreateApp.ts
文件中:
// packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {return {app: null as any,config: { // 应用的配置对象isNativeTag: NO,performance: false,globalProperties: {},optionMergeStrategies: {},isCustomElement: NO,errorHandler: undefined,warnHandler: undefined},mixins: [], // 保存应用内的混入components: {}, // 保存全局组件的信息directives: {}, // 保存全局指令的信息provides: Object.create(null)}
}
分析完 app.component
方法之后,是不是觉得组件注册的过程还是挺简单的。那么对于已注册的组件,何时会被使用呢?要回答这个问题,我们就需要分析另一个步骤 —— 应用挂载。
1.3 应用挂载的过程
为了更加直观地了解应用挂载的过程,阿宝哥利用 Chrome 开发者工具的 Performance 标签栏,记录了应用挂载的主要过程:
在上图中我们发现了一个与组件相关的函数 resolveComponent
。很明显,该函数用于解析组件,且该函数在 render
方法中会被调用。在源码中,我们找到了该函数的定义:
// packages/runtime-core/src/helpers/resolveAssets.ts
const COMPONENTS = 'components'export function resolveComponent(name: string): ConcreteComponent | string {return resolveAsset(COMPONENTS, name) || name
}
由以上代码可知,在 resolveComponent
函数内部,会继续调用 resolveAsset
函数来执行具体的解析操作。在分析 resolveAsset
函数的具体实现之前,我们在 resolveComponent
函数内部加个断点,来一睹 render
方法的 “芳容”:
在上图中,我们看到了解析组件的操作,比如 _resolveComponent("component-a")
。前面我们已经知道在 resolveComponent
函数内部会继续调用 resolveAsset
函数,该函数的具体实现如下:
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(type: typeof COMPONENTS | typeof DIRECTIVES,name: string,warnMissing = true
) {const instance = currentRenderingInstance || currentInstanceif (instance) {const Component = instance.type// 省略大部分处理逻辑const res =// 局部注册// check instance[type] first for components with mixin or extends.resolve(instance[type] || (Component as ComponentOptions)[type], name) ||// 全局注册resolve(instance.appContext[type], name)return res} else if (__DEV__) {warn(`resolve${capitalize(type.slice(0, -1))} ` +`can only be used in render() or setup().`)}
}
因为注册组件时,使用的是全局注册的方式,所以解析的过程会执行 resolve(instance.appContext[type], name)
该语句,其中 resolve
方法的定义如下:
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolve(registry: Record<string, any> | undefined, name: string) {return (registry &&(registry[name] ||registry[camelize(name)] ||registry[capitalize(camelize(name))]))
}
分析完以上的处理流程,我们在解析全局注册的组件时,会通过 resolve
函数从应用的上下文对象中获取已注册的组件对象。
(function anonymous() {const _Vue = Vuereturn function render(_ctx, _cache) {with (_ctx) {const {resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock} = _Vueconst _component_component_a = _resolveComponent("component-a")const _component_component_b = _resolveComponent("component-b")const _component_component_c = _resolveComponent("component-c")return (_openBlock(),_createBlock(_Fragment, null, [_createVNode(_component_component_a), _createVNode(_component_component_b), _createVNode(_component_component_c)], 64))}}
})
在获取到组件之后,会通过 _createVNode
函数创建 VNode
节点。然而,关于 VNode
是如何被渲染成真实的 DOM 元素这个过程,阿宝哥就不继续往下介绍了,后续会写专门的文章来单独介绍这块的内容,接下来我们将介绍动态组件的相关内容。
二、动态组件
在 Vue 3 中为我们提供了一个 component
内置组件,该组件可以渲染一个 “元组件” 为动态组件。根据 is
的值,来决定哪个组件被渲染。如果 is
的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称。对应的使用示例如下:
<!-- 动态组件由 vm 实例的 `componentId` property 控制 -->
<component :is="componentId"></component><!-- 也能够渲染注册过的组件或 prop 传入的组件-->
<component :is="$options.components.child"></component><!-- 可以通过字符串引用组件 -->
<component :is="condition ? 'FooComponent' : 'BarComponent'"></component><!-- 可以用来渲染原生 HTML 元素 -->
<component :is="href ? 'a' : 'span'"></component>
2.1 绑定字符串类型
介绍完 component
内置组件,我们来举个简单的示例:
<div id="app"><buttonv-for="tab in tabs":key="tab"@click="currentTab = 'tab-' + tab.toLowerCase()">{{ tab }}</button><component :is="currentTab"></component>
</div>
<script>const { createApp } = Vueconst tabs = ['Home', 'My']const app = createApp({data() {return {tabs,currentTab: 'tab-' + tabs[0].toLowerCase()}},});app.component('tab-home', {template: `<div style="border: 1px solid;">Home component</div>`})app.component('tab-my', {template: `<div style="border: 1px solid;">My component</div>`})app.mount('#app')
</script>
在以上代码中,我们通过 app.component
方法全局注册了 tab-home
和 tab-my
2 个组件。此外,在模板中,我们使用了 component
内置组件,该组件的 is
属性绑定了 data
对象的 currentTab
属性,该属性的类型是字符串。当用户点击 Tab 按钮时,会动态更新 currentTab
的值,从而实现动态切换组件的功能。以上示例成功运行后的结果如下图所示:
看到这里你会不会觉得 component
内置组件挺神奇的,感兴趣的小伙伴继续跟阿宝哥一起,来揭开它背后的秘密。下面我们利用 Vue 3 Template Explorer 在线工具,看一下 <component :is="currentTab"></component>
模板编译的结果:
const _Vue = Vuereturn function render(_ctx, _cache, $props, $setup, $data, $options) {with (_ctx) {const { resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vuereturn (_openBlock(), _createBlock(_resolveDynamicComponent(currentTab)))}
}
通过观察生成的渲染函数,我们发现了一个 resolveDynamicComponent
的函数,根据该函数的名称,我们可以知道它用于解析动态组件,它被定义在 runtime-core/src/helpers/resolveAssets.ts
文件中,具体实现如下所示:
// packages/runtime-core/src/helpers/resolveAssets.ts
export function resolveDynamicComponent(component: unknown): VNodeTypes {if (isString(component)) {return resolveAsset(COMPONENTS, component, false) || component} else {// invalid types will fallthrough to createVNode and raise warningreturn (component || NULL_DYNAMIC_COMPONENT) as any}
}
在 resolveDynamicComponent
函数内部,若 component
参数是字符串类型,则会调用前面介绍的 resolveAsset
方法来解析组件:
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(type: typeof COMPONENTS | typeof DIRECTIVES,name: string,warnMissing = true
) {const instance = currentRenderingInstance || currentInstanceif (instance) {const Component = instance.type// 省略大部分处理逻辑const res =// 局部注册// check instance[type] first for components with mixin or extends.resolve(instance[type] || (Component as ComponentOptions)[type], name) ||// 全局注册resolve(instance.appContext[type], name)return res}
}
对于前面的示例来说,组件是全局注册的,所以解析过程中会从 app.context
上下文对象的 components
属性中获取对应的组件。当 currentTab
发生变化时,resolveAsset
函数就会返回不同的组件,从而实现动态组件的功能。
此外,如果 resolveAsset
函数获取不到对应的组件,则会返回当前 component
参数的值。比如 resolveDynamicComponent('div')
将返回 'div'
字符串。
// packages/runtime-core/src/helpers/resolveAssets.ts
export const NULL_DYNAMIC_COMPONENT = Symbol()export function resolveDynamicComponent(component: unknown): VNodeTypes {if (isString(component)) {return resolveAsset(COMPONENTS, component, false) || component} else {return (component || NULL_DYNAMIC_COMPONENT) as any}
}
细心的小伙伴可能也注意到了,在 resolveDynamicComponent
函数内部,如果 component
参数非字符串类型,则会返回 component || NULL_DYNAMIC_COMPONENT
这行语句的执行结果,其中 NULL_DYNAMIC_COMPONENT
的值是一个 Symbol 对象。
2.2 绑定对象类型
了解完上述的内容之后,我们来重新实现一下前面动态 Tab 的功能:
<div id="app"><buttonv-for="tab in tabs":key="tab"@click="currentTab = tab">{{ tab.name }}</button><component :is="currentTab.component"></component>
</div>
<script>const { createApp } = Vueconst tabs = [{name: 'Home',component: {template: `<div style="border: 1px solid;">Home component</div>`}},{name: 'My',component: {template: `<div style="border: 1px solid;">My component</div>`}}]const app = createApp({data() {return {tabs,currentTab: tabs[0]}},});app.mount('#app')
</script>
在以上示例中,component
内置组件的 is
属性绑定了 currentTab
对象的 component
属性,该属性的值是一个对象。当用户点击 Tab 按钮时,会动态更新 currentTab
的值,导致 currentTab.component
的值也发生变化,从而实现动态切换组件的功能。需要注意的是,每次切换的时候,都会重新创建动态组件。但在某些场景下,你会希望保持这些组件的状态,以避免反复重渲染导致的性能问题。
对于这个问题,我们可以使用 Vue 3 的另一个内置组件 —— keep-alive
,将动态组件包裹起来。比如:
<keep-alive><component :is="currentTab"></component>
</keep-alive>
keep-alive
内置组件的主要作用是用于保留组件状态或避免重新渲染,使用它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。关于 keep-alive
组件的内部工作原理,阿宝哥后面会写专门的文章来分析它,对它感兴趣的小伙伴记得关注 Vue 3.0 进阶 系列哟。
三、阿宝哥有话说
3.1 除了 component 内置组件外,还有哪些内置组件?
在 Vue 3 中除了本文介绍的 component
和 keep-alive
内置组件之外,还提供了 transition
、transition-group
、slot
和 teleport
内置组件。
3.2 注册全局组件与局部组件有什么区别?
注册全局组件
const { createApp, h } = Vue
const app = createApp({});
app.component('component-a', {template: "<p>我是组件A</p>"
});
使用 app.component
方法注册的全局的组件,被保存到 app
应用对象的上下文对象中。而通过组件对象 components
属性注册的局部组件是保存在组件实例中。
注册局部组件
const { createApp, h } = Vue
const app = createApp({});
const componentA = () => h('div', '我是组件A');
app.component('component-b', {components: {'component-a': componentA},template: `<div>我是组件B,内部使用了组件A<component-a></component-a> </div>`
})
解析全局注册和局部注册的组件
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(type: typeof COMPONENTS | typeof DIRECTIVES,name: string,warnMissing = true
) {const instance = currentRenderingInstance || currentInstanceif (instance) {const Component = instance.type// 省略大部分处理逻辑const res =// 局部注册// check instance[type] first for components with mixin or extends.resolve(instance[type] || (Component as ComponentOptions)[type], name) ||// 全局注册resolve(instance.appContext[type], name)return res}
}
3.3 动态组件能否绑定其他属性?
component
内置组件除了支持 is
绑定之外,也支持其他属性绑定和事件绑定:
<component :is="currentTab.component" :name="name" @click="sayHi"></component>
这里阿宝哥使用 Vue 3 Template Explorer 这个在线工具,来编译上述的模板:
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {with (_ctx) {const { resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vuereturn (_openBlock(), _createBlock(_resolveDynamicComponent(currentTab.component), {name: name,onClick: sayHi}, null, 8 /* PROPS */, ["name", "onClick"]))}
}
观察以上的渲染函数可知,除了 is
绑定会被转换为 _resolveDynamicComponent
函数调用之外,其他的属性绑定都会被正常解析为 props
对象。
四、参考资源
Vue 3 官网 - 应用 API
Vue 3 官网 - 内置组件
聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。
【Vue.js】892- Vue 3.0 进阶之动态组件探秘相关推荐
- 【Vue.js】900- Vue 3.0 进阶之 VNode 探秘
本文是 Vue 3.0 进阶系列 的第五篇文章,在这篇文章中,阿宝哥将介绍 Vue 3 中的核心对象 -- VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node).与 & ...
- Vue.js 框架源码与进阶 - Vue.js 源码剖析 - 响应式原理
文章目录 一.准备工作 1.1 Vue 源码的获取 1.2 源目录结构 1.3 了解 Flow 1.4 调试设置 1.5 Vue 的不同构建版本 1.6 寻找入口文件 1.7 从入口开始 二.Vue ...
- Vue.js(一) Vue.js + element-ui 扫盲
Vue.js(一) Vue.js + element-ui 扫盲 2018年12月09日 20:32:59 vbirdbest 阅读数 7043更多 分类专栏: Vue.js + ElementUI ...
- vue.js:597 [Vue warn]: Error in callback for watcher dat: TypeError: Cannot read property 'call'
vue.js:597 [Vue warn]: Error in callback for watcher "dat": "TypeError: Cannot read p ...
- vue.js:590 [Vue tip]: Event “removeitem“ is emitted in component <TodoItems> but the handler is regi
报错信息 vue.js:590 [Vue tip]: Event "removeitem" is emitted in component <TodoItems> bu ...
- vue.js报错 vue.js:597 [Vue warn]: Cannot find element: #app
刚开始使用vue的时候发现vue报vue.js:597 [Vue warn]: Cannot find element: #app的错误,初始以为是写的代码有错误导致,于是认真的对照了几遍发现代码和官 ...
- Vue.js教程-Vue项目的目录结构和.vue文件的构成
Vue.js教程-Vue项目的目录结构和.vue文件的构成 前言 Vue项目的目录结构(Vue-cli3/4版本) .vue文件的构成 Html区域(template) script区域 export ...
- vue在html中执行js代码,Vue.js 和 Vue.runtime.js
Vue官方中文文档: Vue有两个版本: 完整版:vue.js.vue.min.js(运行时版+编译器)(编译器:将模板字符串编译成为JS渲染函数的代码) 运行时版:vue.runtime.js.vu ...
- Vue.js 框架源码与进阶 - 搭建自己的SSR
文章目录 一.Vue SSR 介绍 1.1 Vue SSR 是什么 1.2 使用场景 1.3 如何实现 Vue SSR 二.Vue SSR 基本使用 2.1 渲染一个 Vue 实例 2.2 与服务器集 ...
最新文章
- AS3 Drawing API演讲稿
- 【Alpha】Daily Scrum Meeting第七次
- python第七关再来一盘_Python爬虫自学系列(七) — 项目实战篇(一)
- 《程序设计与数据结构》第6周学习总结
- 如何结合SharePoint Designer 2010 与Visio 2010 创建工作流?
- opencv生成日志_OpenCV在Android环境下的使用方法
- 如何监视SQL Server索引的总大小
- leetcode 368
- Java集合---ConcurrentHashMap原理分析
- 空集有四种写法,你知道么?——常用Latex符号表来啦!
- 《Vim实用技巧》笔记
- 将整数翻译成英文(C++)
- Java里线程的隔离方式_线程隔离浅析
- NTL密码算法开源库-大整数ZZ类(三)
- windows同步linux用户,用winbind实现windowsPDC和linux系统的帐号同步
- 滤波反投影重建算法(FBP)实现及应用(matlab)
- 在Linux服务器上安装SQL Server
- 日内趋势票如何把握?
- 工业物联网安全需要一揽子服务商
- Qt将选中的字体加粗下划线