1. 程式人生 > 程式設計 >vue3的動態元件是如何工作的

vue3的動態元件是如何工作的

在這篇文章中,阿寶哥將介紹 vue 3 中的內建元件 —— component,該元件的作用是渲染一個 “元元件” 為動態元件。如果你對動態元件還不瞭解的話也沒關係,文中阿寶哥會通過具體的示例,來介紹動態元件的應用。由於動態元件內部與元件註冊之間有一定的聯絡,所以為了讓大家能夠更好地瞭解動態元件的內部原理,阿寶哥會先介紹元件註冊的相關知識。

一、元件註冊

1.1 全域性註冊

在 Vue 3.0 中,通過使用 app 物件的 component 方法,可以很容易地註冊或檢索全域性元件。component 方法支援兩個引數:

  • name:元件名稱;FrZKsgCNG
  • component:元件定義物件。

接下來,我們來看一個簡單的示例:

<div id="app">
 <component-a></component-a>
 <component-b></component-b>
 <component-c></component-c>
</div>
<script>
 const { createApp } = Vue
 const 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 = false

 const 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 屬性中,具體如下圖所示:

vue3的動態元件是如何工作的

而 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 應用掛載的過程

www.cppcns.com了更加直觀地瞭解應用掛載的過程,阿寶哥利用 Chrome 開發者工具的 Performance 標籤欄,記錄了應用掛載的主要過程:

vue3的動態元件是如何工作的

在上圖中我們發現了一個與元件相關的函式 resolveComponent。很明顯,該函式用於解析元件,且該函式在 render 方法中會被呼叫。在原始碼中,我們找到了該函式的定義:

// packages/runtime-core/src/helpers/resolveAssets.ts
const COMPONENTS = 'components'

export function resolveComponent(na程式設計客棧me: string): ConcreteComponent | string {
 return resolveAsset(COMPONENTS,name) || name
}

由以上程式碼可知,在 resolveComponent 函式內部,會繼續呼叫 resolveAsset 函式來執行具體的解析操作。在分析 resolveAsset 函式的具體實現之前,我們在 resolveComponent 函式內部加個斷點,來一睹 render 方法的 “芳容”:

vue3的動態元件是如何工作的

在上圖中,我們看到了解析元件的操作,比如 _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 || currentInstance
 if (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 = Vue

 return function render(_ctx,_cache) {
  with (_ctx) {
   const {resolveComponent: _resolveComponent,createVNode: _createVNode,Fragment: _Fragment,openBlock: _openBlock,createBlock: _createBlock} = _Vue

   const _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">
 <button
  v-for="tab in tabs"
  :key="tab"
  @click="currentTab = 'tab-' + tab.toLowerCase()">
  {{ tab }}
 </button>
 <component :is="currentTab"></component>
</div>
<script>
 const { createApp } = Vue
 const 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 的值,從而實現動態切換元件的功能。以上示例成功執行後的結果如下圖所示:

vue3的動態元件是如何工作的

看到這裡你會不會覺得 component 內建元件挺神奇的,感興趣的小夥伴繼續跟阿寶哥一起,來揭開它背後的祕密。下面我們利用 Vue 3 Template Explorer 線上工具,看一下 <component :is="currentTab"></component> 模板編譯的結果:

const _Vue = Vue

return function render(_ctx,_cache,$props,$setup,$data,$options) {
 with (_ctx) {
 const { resolveDynamicComponent: _resolveDynamicComponent,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(component: unknown): VNodeTypes {
 if (isString(component)) {
 return resolveAsset(COMPONENTS,component,false) || component
 } else {
 // invalid types will fallthrough to createVNode and raise warning
 return (component || NULL_DYNAMIC_COMPONENT) as any
 }
}

在 resolveDynamicComponent 函式內部,若 component 引數是字串型別,則會呼叫前面介紹的 resolveAsset 方法來解析元件:

// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
 type: typeof COMPONENTS | typeof DIRECTIVES,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,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">
 <button
  v-for="tab in tabs"
  :key="tab"
  @click="currentTab = tab">
  {{ tab.name }}
 </button>
 <component :is="currentTab.component"></component>
</div>
<script>
 const { createApp } = Vue
 const tabs = [
  {
  name: 'Home',component: {
   template: `<div style="border: 1px solid;">Home component</div>`
  }
  },{
  name: 'My',component: {
   template: `<div style="http://www.cppcns.comborder: 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 和 keewww.cppcns.comp-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)
 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,createBlock: _createBlock } = _Vue

 return (_openBlock(),_createBlock(_resolveDynamicComponent(currentTab.component),{
  name: name,onClick: sayHi
 },8 /* PROPS */,["name","onClick"]))
 }
}

觀察以上的渲染函式可知,除了 is 繫結會被轉換為 _resolveDynamicComponent 函式呼叫之外,其他的屬性繫結都會被正常解析為 props 物件。

以上就是vue3的動態元件是如何工作的的詳細內容,更多關於vue3動態元件的資料請關注我們其它相關文章!