vue3的动态组件是如何工作的
在这篇文章中,阿宝哥将介绍 Vue 3 中的内置组件 —— ponent,该组件的作用是渲染一个 “元组件” 为动态组件。如果你对动态组件还不了解的话也没关系,文中阿宝哥会通过具体的示例,来介绍动态组件的应用。由于动态组件内部与组件注册之间有一定的联系,所以为了让大家能够更好地了解动态组件的内部原理,阿宝哥会先介绍组件注册的相关知识。
一、组件注册
1.1 全局注册
在 Vue 3.0 中,通过使用 app 对象的 ponent 方法,可以很容易地注册或检索全局组件。ponent 方法支持两个参数
- name组件名称;
- ponent组件定义对象。
接下来,我们来看一个简单的示例
<div id="app"> <ponent-a></ponent-a> <ponent-b></ponent-b> <ponent-c></ponent-c> </div> <script> const { createApp } = Vue const app = createApp({}); // ① app.ponent('ponent-a', { // ② template: "<p>我是组件A</p>" }); app.ponent('ponent-b', { template: "<p>我是组件B</p>" }); app.ponent('ponent-c', { template: "<p>我是组件C</p>" }); app.mount('#app') // ③ </script>
在以上代码中,我们通过 app.ponent 方法注册了 3 个组件,这些组件都是全局注册的 。也就是说它们在注册之后可以用在任何新创建的组件实例的模板中。该示例的代码比较简单,主要包含 3 个步骤创建 App 对象、注册全局组件和应用挂载。其中创建 App 对象的细节,阿宝哥会在后续的文章中单独介绍,狼蚁网站SEO优化我们将重点分析其他 2 个步骤,我们先来分析注册全局组件的过程。
1.2 注册全局组件的过程
在以上示例中,我们使用 app 对象的 ponent 方法来注册全局组件
app.ponent('ponent-a', { template: "<p>我是组件A</p>" });
,除了注册全局组件之外,我们也可以注册局部组件,因为组件中也接受一个 ponents 的选项
const app = Vue.createApp({ ponents: { 'ponent-a': ComponentA, 'ponent-b': ComponentB } })
需要注意的是,局部注册的组件在其子组件中是不可用的。接下来,我们来继续介绍注册全局组件的过程。对于前面的示例来说,我们使用的 app.ponent 方法被定义在 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 = false const app: App = (context.app = { // 省略部分代码 _context: context, // 注册或检索全局组件 ponent(name: string, ponent?: Component): any { if (__DEV__) { validateComponentName(name, context.config) } if (!ponent) { // 获取name对应的组件 return context.ponents[name] } if (__DEV__ && context.ponents[name]) { // 重复注册提示 warn(`Component "${name}" has already been registered in target app.`) } context.ponents[name] = ponent // 注册全局组件 return app }, }) return app } }
当所有的组件都注册成功之后,它们会被保存到 context 对象的 ponents 属性中,具体如下图所示
而 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: [], // 保存应用内的混入 ponents: {}, // 保存全局组件的信息 directives: {}, // 保存全局指令的信息 provides: Object.create(null) } }
分析完 app.ponent 方法之后,是不是觉得组件注册的过程还是挺简单的。那么对于已注册的组件,何时会被使用呢?要回答这个问题,我们就需要分析另一个步骤 —— 应用挂载。
1.3 应用挂载的过程
为了更加直观地了解应用挂载的过程,阿宝哥利用 Chrome 开发者工具的 Performance 标签栏,记录了应用挂载的主要过程
在上图中我们发现了一个与组件相关的函数 resolveComponent。很明显,该函数用于解析组件,且该函数在 render 方法中会被调用。在源码中,我们找到了该函数的定义
// packages/runtime-core/src/helpers/resolveAssets.ts const COMPONENTS = 'ponents' export function resolveComponent(name: string): ConcreteComponent | string { return resolveAsset(COMPONENTS, name) || name }
由以上代码可知,在 resolveComponent 函数内部,会继续调用 resolveAsset 函数来执行具体的解析操作。在分析 resolveAsset 函数的具体实现之前,我们在 resolveComponent 函数内部加个断点,来一睹 render 方法的 “芳容”
在上图中,我们看到了解析组件的操作,比如 _resolveComponent("ponent-a")。前面我们已经知道在 resolveComponent 函数内部会继续调用 resolveAsset 函数,该函数的具体实现如下
// packages/runtime-core/src/helpers/resolveAssets.ts function resolveAsset( type: typeof COMPONENTS | typeof DIRECTIVES, name: string, warnMissing = true ) { const instance = currentRenderingInstance || currentInstance if (instance) { const Component = instance.type // 省略大部分处理逻辑 const res = // 局部注册 // check instance[type] first for ponents 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 = Vue return function render(_ctx, _cache) { with (_ctx) { const {resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock} = _Vue const _ponent_ponent_a = _resolveComponent("ponent-a") const _ponent_ponent_b = _resolveComponent("ponent-b") const _ponent_ponent_c = _resolveComponent("ponent-c") return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode(_ponent_ponent_a), _createVNode(_ponent_ponent_b), _createVNode(_ponent_ponent_c)], 64)) } } })
在获取到组件之后,会通过 _createVNode 函数创建 VNode 节点。,关于 VNode 是如何被渲染成真实的 DOM 元素这个过程,阿宝哥就不继续往下介绍了,后续会写专门的文章来单独介绍这块的内容,接下来我们将介绍动态组件的相关内容。
二、动态组件
在 Vue 3 中为我们提供了一个 ponent 内置组件,该组件可以渲染一个 “元组件” 为动态组件。根据 is 的值,来决定哪个组件被渲染。如果 is 的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称。对应的使用示例如下
<!-- 动态组件由 vm 实例的 `ponentId` property 控制 --> <ponent :is="ponentId"></ponent> <!-- 也能够渲染注册过的组件或 prop 传入的组件--> <ponent :is="$options.ponents.child"></ponent> <!-- 可以通过字符串引用组件 --> <ponent :is="condition ? 'FooComponent' : 'BarComponent'"></ponent> <!-- 可以用来渲染原生 HTML 元素 --> <ponent :is="href ? 'a' : 'span'"></ponent>
2.1 绑定字符串类型
介绍完 ponent 内置组件,我们来举个简单的示例
<div id="app"> <button v-for="tab in tabs" :key="tab" @click="currentTab = 'tab-' + tab.toLowerCase()"> {{ tab }} </button> <ponent :is="currentTab"></ponent> </div> <script> const { createApp } = Vue const tabs = ['Home', 'My'] const app = createApp({ data() { return { tabs, currentTab: 'tab-' + tabs[0].toLowerCase() } }, }); app.ponent('tab-home', { template: `<div style="border: 1px solid;">Home ponent</div>` }) app.ponent('tab-my', { template: `<div style="border: 1px solid;">My ponent</div>` }) app.mount('#app') </script>
在以上代码中,我们通过 app.ponent 方法全局注册了 tab-home 和 tab-my 2 个组件。,在模板中,我们使用了 ponent 内置组件,该组件的 is 属性绑定了 data 对象的 currentTab 属性,该属性的类型是字符串。当用户点击 Tab 按钮时,会动态更新 currentTab 的值,从而实现动态切换组件的功能。以上示例成功运行后的结果如下图所示
看到这里你会不会觉得 ponent 内置组件挺神奇的,感兴趣的小伙伴继续跟阿宝哥一起,来揭开它背后的秘密。狼蚁网站SEO优化我们利用 在线工具,看一下 <ponent :is="currentTab"></ponent> 模板编译的结果
const _Vue = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { with (_ctx) { const { resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(_resolveDynamicComponent(currentTab))) } }
通过观察生成的渲染函数,我们发现了一个 resolveDynamicComponent 的函数,根据该函数的名称,我们可以知道它用于解析动态组件,它被定义在 runtime-core/src/helpers/resolveAssets.ts 文件中,具体实现如下所示
// packages/runtime-core/src/helpers/resolveAssets.ts export function resolveDynamicComponent(ponent: unknown): VNodeTypes { if (isString(ponent)) { return resolveAsset(COMPONENTS, ponent, false) || ponent } else { // invalid types will fallthrough to createVNode and raise warning return (ponent || NULL_DYNAMIC_COMPONENT) as any } }
在 resolveDynamicComponent 函数内部,若 ponent 参数是字符串类型,则会调用前面介绍的 resolveAsset 方法来解析组件
// packages/runtime-core/src/helpers/resolveAssets.ts function resolveAsset( type: typeof COMPONENTS | typeof DIRECTIVES, name: string, warnMissing = true ) { const instance = currentRenderingInstance || currentInstance if (instance) { const Component = instance.type // 省略大部分处理逻辑 const res = // 局部注册 // check instance[type] first for ponents with mixin or extends. resolve(instance[type] || (Component as ComponentOptions)[type], name) || // 全局注册 resolve(instance.appContext[type], name) return res } }
对于前面的示例来说,组件是全局注册的,所以解析过程中会从 app.context 上下文对象的 ponents 属性中获取对应的组件。当 currentTab 发生变化时,resolveAsset 函数就会返回不同的组件,从而实现动态组件的功能。,如果 resolveAsset 函数获取不到对应的组件,则会返回当前 ponent 参数的值。比如 resolveDynamicComponent('div') 将返回 'div' 字符串。
// packages/runtime-core/src/helpers/resolveAssets.ts export const NULL_DYNAMIC_COMPONENT = Symbol() export function resolveDynamicComponent(ponent: unknown): VNodeTypes { if (isString(ponent)) { return resolveAsset(COMPONENTS, ponent, false) || ponent } else { return (ponent || NULL_DYNAMIC_COMPONENT) as any } }
细心的小伙伴可能也注意到了,在 resolveDynamicComponent 函数内部,如果 ponent 参数非字符串类型,则会返回 ponent || NULL_DYNAMIC_COMPONENT 这行语句的执行结果,其中 NULL_DYNAMIC_COMPONENT 的值是一个 Symbol 对象。
2.2 绑定对象类型
了解完上述的内容之后,我们来重新实现一下前面动态 Tab 的功能
<div id="app"> <button v-for="tab in tabs" :key="tab" @click="currentTab = tab"> {{ tab.name }} </button> <ponent :is="currentTab.ponent"></ponent> </div> <script> const { createApp } = Vue const tabs = [ { name: 'Home', ponent: { template: `<div style="border: 1px solid;">Home ponent</div>` } }, { name: 'My', ponent: { template: `<div style="border: 1px solid;">My ponent</div>` } }] const app = createApp({ data() { return { tabs, currentTab: tabs[0] } }, }); app.mount('#app') </script>
在以上示例中,ponent 内置组件的 is 属性绑定了 currentTab 对象的 ponent 属性,该属性的值是一个对象。当用户点击 Tab 按钮时,会动态更新 currentTab 的值,导致 currentTab.ponent 的值也发生变化,从而实现动态切换组件的功能。需要注意的是,每次切换的时候,都会重新创建动态组件。但在某些场景下,你会希望保持这些组件的状态,以避免反复重渲染导致的性能问题。
对于这个问题,我们可以使用 Vue 3 的另一个内置组件 —— keep-alive,将动态组件包裹起来。比如
<keep-alive> <ponent :is="currentTab"></ponent> </keep-alive>
keep-alive 内置组件的主要作用是用于保留组件状态或避免重新渲染,使用它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。关于 keep-alive 组件的内部工作原理,阿宝哥后面会写专门的文章来分析它,对它感兴趣的小伙伴记得关注 Vue 3.0 进阶 系列哟。
三、阿宝哥有话说
3.1 除了 ponent 内置组件外,还有哪些内置组件?
在 Vue 3 中除了本文介绍的 ponent 和 keep-alive 内置组件之外,还提供了 transition、transition-group 、slot 和 teleport 内置组件。
3.2 注册全局组件与局部组件有什么区别?
注册全局组件
const { createApp, h } = Vue const app = createApp({}); app.ponent('ponent-a', { template: "<p>我是组件A</p>" });
使用 app.ponent 方法注册的全局的组件,被保存到 app 应用对象的上下文对象中。而通过组件对象 ponents 属性注册的局部组件是保存在组件实例中。
注册局部组件
const { createApp, h } = Vue const app = createApp({}); const ponentA = () => h('div', '我是组件A'); app.ponent('ponent-b', { ponents: { 'ponent-a': ponentA }, template: `<div> 我是组件B,内部使用了组件A <ponent-a></ponent-a> </div>` })
解析全局注册和局部注册的组件
// packages/runtime-core/src/helpers/resolveAssets.ts function resolveAsset( type: typeof COMPONENTS | typeof DIRECTIVES, name: string, warnMissing = true ) { const instance = currentRenderingInstance || currentInstance if (instance) { const Component = instance.type // 省略大部分处理逻辑 const res = // 局部注册 // check instance[type] first for ponents with mixin or extends. resolve(instance[type] || (Component as ComponentOptions)[type], name) || // 全局注册 resolve(instance.appContext[type], name) return res } }
3.3 动态组件能否绑定其他属性?
ponent 内置组件除了支持 is 绑定之外,也支持其他属性绑定和事件绑定
<ponent :is="currentTab.ponent" :name="name" @click="sayHi"></ponent>
这里阿宝哥使用 这个在线工具,来编译上述的模板
const _Vue = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { with (_ctx) { const { resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(_resolveDynamicComponent(currentTab.ponent), { name: name, onClick: sayHi }, null, 8 / PROPS /, ["name", "onClick"])) } }
观察以上的渲染函数可知,除了 is 绑定会被转换为 _resolveDynamicComponent 函数调用之外,其他的属性绑定都会被正常解析为 props 对象。
以上就是vue3的动态组件是如何工作的的详细内容,更多关于vue3动态组件的资料请关注狼蚁SEO其它相关文章!
编程语言
- 如何快速学会编程 如何快速学会ug编程
- 免费学编程的app 推荐12个免费学编程的好网站
- 电脑怎么编程:电脑怎么编程网咯游戏菜单图标
- 如何写代码新手教学 如何写代码新手教学手机
- 基础编程入门教程视频 基础编程入门教程视频华
- 编程演示:编程演示浦丰投针过程
- 乐高编程加盟 乐高积木编程加盟
- 跟我学plc编程 plc编程自学入门视频教程
- ug编程成航林总 ug编程实战视频
- 孩子学编程的好处和坏处
- 初学者学编程该从哪里开始 新手学编程从哪里入
- 慢走丝编程 慢走丝编程难学吗
- 国内十强少儿编程机构 中国少儿编程机构十强有
- 成人计算机速成培训班 成人计算机速成培训班办
- 孩子学编程网上课程哪家好 儿童学编程比较好的
- 代码编程教学入门软件 代码编程教程