溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue3中的依賴注入與組件定義怎么實現(xiàn)

發(fā)布時間:2023-03-22 09:17:46 來源:億速云 閱讀:96 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“Vue3中的依賴注入與組件定義怎么實現(xiàn)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“Vue3中的依賴注入與組件定義怎么實現(xiàn)”吧!

provide() & inject()

provide()

提供一個值,可以被后代組件注入。

function provide<T>(key: InjectionKey<T> | string, value: T): void

接收兩個參數(shù):

  • 要注入的 key,字符串或者 Symbol;

export interface InjectionKey<T> extends Symbol {}

  • 對應(yīng)注入的值

與注冊生命周期鉤子的 API 類似,provide() 必須在組件的 setup() 階段同步調(diào)用。

inject()

注入一個由祖先組件或整個應(yīng)用 (通過 app.provide()) 提供的值。

// 沒有默認(rèn)值
function inject<T>(key: InjectionKey<T> | string): T | undefined

// 帶有默認(rèn)值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

// 使用工廠函數(shù)
function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: () => T,
  treatDefaultAsFactory: true
): T

  • 第一個參數(shù)是注入的 keyVue 會遍歷父組件鏈,通過匹配 key 來確定所提供的值。如果父組件鏈上多個組件對同一個 key 提供了值,那么離得更近的組件將會“覆蓋”鏈上更遠(yuǎn)的組件所提供的值。如果沒有能通過 key 匹配到值,inject() 將返回 undefined,除非提供了一個默認(rèn)值。

  • 第二個參數(shù)是可選的,即在沒有匹配到 key 時使用的默認(rèn)值。它也可以是一個工廠函數(shù),用來返回某些創(chuàng)建起來比較復(fù)雜的值。如果默認(rèn)值本身就是一個函數(shù),那么你必須將 false 作為第三個參數(shù)傳入,表明這個函數(shù)就是默認(rèn)值,而不是一個工廠函數(shù)。

provide() & inject() - 官方示例

// provide
<script setup>
  import {(ref, provide)} from 'vue' import {fooSymbol} from
  './injectionSymbols' // 提供靜態(tài)值 provide('foo', 'bar') // 提供響應(yīng)式的值
  const count = ref(0) provide('count', count) // 提供時將 Symbol 作為 key
  provide(fooSymbol, count)
</script>
// inject
<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'

// 注入值的默認(rèn)方式
const foo = inject('foo')

// 注入響應(yīng)式的值
const count = inject('count')

// 通過 Symbol 類型的 key 注入
const foo2 = inject(fooSymbol)

// 注入一個值,若為空則使用提供的默認(rèn)值
const bar = inject('foo', 'default value')

// 注入一個值,若為空則使用提供的工廠函數(shù)
const baz = inject('foo', () => new Map())

// 注入時為了表明提供的默認(rèn)值是個函數(shù),需要傳入第三個參數(shù)
const fn = inject('function', () => {}, false)
</script>

provide() & inject() - ElementUI Plus 示例 Breadcrumb 組件

<script setup>
import { onMounted, provide, ref } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { breadcrumbKey } from './constants'
import { breadcrumbProps } from './breadcrumb'

defineOptions({
  name: 'ElBreadcrumb',
})

const props = defineProps(breadcrumbProps)
const ns = useNamespace('breadcrumb')
const breadcrumb = ref<HTMLDivElement>()
// 提供值
provide(breadcrumbKey, props)

onMounted(() => {
  ......
})
</script>
<script setup>
import { getCurrentInstance, inject, ref, toRefs } from 'vue'
import ElIcon from '@element-plus/components/icon'
import { useNamespace } from '@element-plus/hooks'
import { breadcrumbKey } from './constants'
import { breadcrumbItemProps } from './breadcrumb-item'

import type { Router } from 'vue-router'

defineOptions({
  name: 'ElBreadcrumbItem',
})

const props = defineProps(breadcrumbItemProps)

const instance = getCurrentInstance()!
// 注入值
const breadcrumbContext = inject(breadcrumbKey, undefined)!
const ns = useNamespace('breadcrumb')
 ......
</script>

provide() & inject() - VueUse 示例

createInjectionState 源碼 / createInjectionState 使用

package/core/computedInject 源碼

import { type InjectionKey, inject, provide } from 'vue-demi'

/**
 * 創(chuàng)建可以注入到組件中的全局狀態(tài)
 */
export function createInjectionState<Arguments extends Array<any>, Return>(
  composable: (...args: Arguments) => Return
): readonly [
  useProvidingState: (...args: Arguments) => Return,
  useInjectedState: () => Return | undefined
] {
  const key: string | InjectionKey<Return> = Symbol('InjectionState')
  const useProvidingState = (...args: Arguments) => {
    const state = composable(...args)
    provide(key, state)
    return state
  }
  const useInjectedState = () => inject(key)
  return [useProvidingState, useInjectedState]
}

nextTick()

等待下一次 DOM 更新刷新的工具方法。

function nextTick(callback?: () => void): Promise<void>

說明:當(dāng)你在 Vue 中更改響應(yīng)式狀態(tài)時,最終的 DOM 更新并不是同步生效的,而是由 Vue 將它們緩存在一個隊列中,直到下一個“tick”才一起執(zhí)行。這樣是為了確保每個組件無論發(fā)生多少狀態(tài)改變,都僅執(zhí)行一次更新。

nextTick() 可以在狀態(tài)改變后立即使用,以等待 DOM 更新完成。你可以傳遞一個回調(diào)函數(shù)作為參數(shù),或者 await 返回的 Promise。

nextTick() 官網(wǎng)示例

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++

  // DOM 還未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此時已經(jīng)更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

<template>
  <button id="counter" @click="increment">{{ count }}</button>
</template>

nextTick() - ElementUI Plus 示例

ElCascaderPanel 源碼

export default defineComponent({
  ......
  const syncMenuState = (
    newCheckedNodes: CascaderNode[],
    reserveExpandingState = true
  ) => {
    ......
    checkedNodes.value = newNodes
    nextTick(scrollToExpandingNode)
  }
  const scrollToExpandingNode = () => {
    if (!isClient) return
    menuList.value.forEach((menu) => {
      const menuElement = menu?.$el
      if (menuElement) {
        const container = menuElement.querySelector(`.${ns.namespace.value}-scrollbar__wrap`)
        const activeNode = menuElement.querySelector(`.${ns.b('node')}.${ns.is('active')}`) ||
          menuElement.querySelector(`.${ns.b('node')}.in-active-path`)
        scrollIntoView(container, activeNode)
      }
    })
  }
  ......
})

nextTick() - VueUse 示例

useInfiniteScroll 源碼

export function useInfiniteScroll(
  element: MaybeComputedRef<HTMLElement | SVGElement | Window | Document | null | undefined>
  ......
) {
  const state = reactive(......)
  watch(
    () => state.arrivedState[direction],
    async (v) => {
      if (v) {
        const elem = resolveUnref(element) as Element
        ......
        if (options.preserveScrollPosition && elem) {
          nextTick(() => {
            elem.scrollTo({
              top: elem.scrollHeight - previous.height,
              left: elem.scrollWidth - previous.width,
            })
          })
        }
      }
    }
  )
}

使用場景:
  • 當(dāng)你需要在修改了某些數(shù)據(jù)后立即對 DOM 進(jìn)行操作時,可以使用 nextTick 來確保 DOM 已經(jīng)更新完畢。例如,在使用 $ref 獲取元素時,需要確保元素已經(jīng)被渲染才能夠正確獲取。

  • 在一些復(fù)雜頁面中,有些組件可能會因為條件渲染或動態(tài)數(shù)據(jù)而頻繁地變化。使用 nextTick 可以避免頻繁地進(jìn)行 DOM 操作,從而提高應(yīng)用程序的性能。

  • 當(dāng)需要在模板中訪問某些計算屬性或者監(jiān)聽器中的值時,也可以使用 nextTick 來確保這些值已經(jīng)更新完畢。這樣可以避免在視圖中訪問到舊值。

總之,nextTick 是一個非常有用的 API,可以確保在正確的時機(jī)對 DOM 進(jìn)行操作,避免出現(xiàn)一些不必要的問題,并且可以提高應(yīng)用程序的性能。

defineComponent()

在定義 Vue 組件時提供類型推導(dǎo)的輔助函數(shù)。

function defineComponent(
  component: ComponentOptions | ComponentOptions['setup']
): ComponentConstructor

第一個參數(shù)是一個組件選項對象。返回值將是該選項對象本身,因為該函數(shù)實際上在運(yùn)行時沒有任何操作,僅用于提供類型推導(dǎo)。

注意返回值的類型有一點特別:它會是一個構(gòu)造函數(shù)類型,它的實例類型是根據(jù)選項推斷出的組件實例類型。這是為了能讓該返回值在 TSX 中用作標(biāo)簽時提供類型推導(dǎo)支持。

const Foo = defineComponent(/* ... */)
// 提取出一個組件的實例類型 (與其選項中的 this 的類型等價)
type FooInstance = InstanceType<typeof Foo>

參考:Vue3 - defineComponent 解決了什么?

defineComponent() - ElementUI Plus 示例

ConfigProvider 源碼

import { defineComponent, renderSlot, watch } from 'vue'
import { provideGlobalConfig } from './hooks/use-global-config'
import { configProviderProps } from './config-provider-props'
......
const ConfigProvider = defineComponent({
  name: 'ElConfigProvider',
  props: configProviderProps,

  setup(props, { slots }) {
    ......
  },
})
export type ConfigProviderInstance = InstanceType<typeof ConfigProvider>

export default ConfigProvider

defineComponent() - Treeshaking

因為 defineComponent() 是一個函數(shù)調(diào)用,所以它可能被某些構(gòu)建工具認(rèn)為會產(chǎn)生副作用,如 webpack。即使一個組件從未被使用,也有可能不被 tree-shake

為了告訴 webpack 這個函數(shù)調(diào)用可以被安全地 tree-shake,我們可以在函數(shù)調(diào)用之前添加一個 /_#**PURE**_/ 形式的注釋:

export default /*#__PURE__*/ defineComponent(/* ... */)

請注意,如果你的項目中使用的是 Vite,就不需要這么做,因為 Rollup (Vite 底層使用的生產(chǎn)環(huán)境打包工具) 可以智能地確定 defineComponent() 實際上并沒有副作用,所以無需手動注釋。

defineComponent() - VueUse 示例

OnClickOutside 源碼

import { defineComponent, h, ref } from 'vue-demi'
import { onClickOutside } from '@vueuse/core'
import type { RenderableComponent } from '../types'
import type { OnClickOutsideOptions } from '.'
export interface OnClickOutsideProps extends RenderableComponent {
  options?: OnClickOutsideOptions
}
export const OnClickOutside = /* #__PURE__ */ defineComponent<OnClickOutsideProps>({
    name: 'OnClickOutside',
    props: ['as', 'options'] as unknown as undefined,
    emits: ['trigger'],
    setup(props, { slots, emit }) {
      ... ...

      return () => {
        if (slots.default)
          return h(props.as || 'div', { ref: target }, slots.default())
      }
    },
  })

defineAsyncComponent()

定義一個異步組件,它在運(yùn)行時是懶加載的。參數(shù)可以是一個異步加載函數(shù),或是對加載行為進(jìn)行更具體定制的一個選項對象。

function defineAsyncComponent(
  source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
  loader: AsyncComponentLoader
  loadingComponent?: Component
  errorComponent?: Component
  delay?: number
  timeout?: number
  suspensible?: boolean
  onError?: (
    error: Error,
    retry: () => void,
    fail: () => void,
    attempts: number
  ) => any
}

defineAsyncComponent() - 官網(wǎng)示例

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    resolve(/* 從服務(wù)器獲取到的組件 */)
  })
})

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>
<template>
  <AsyncComp />
  <AdminPage />
</template>

ES 模塊動態(tài)導(dǎo)入也會返回一個 Promise,所以多數(shù)情況下我們會將它和 defineAsyncComponent 搭配使用。類似 ViteWebpack 這樣的構(gòu)建工具也支持此語法 (并且會將它們作為打包時的代碼分割點),因此我們也可以用它來導(dǎo)入 Vue 單文件組件。

defineAsyncComponent() - VitePress 示例

<script setup>
import { defineAsyncComponent } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
defineProps<{ carbonAds: DefaultTheme.CarbonAdsOptions }>()
const VPCarbonAds = __CARBON__
  ? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
  : () => null
</script>
<template>
  <div>
    <VPCarbonAds :carbon-ads="carbonAds" />
  </div>
</template>

defineAsyncComponent()使用場景:

  • 當(dāng)你需要異步加載某些組件時,可以使用 defineAsyncComponent 來進(jìn)行組件懶加載,這樣可以提高應(yīng)用程序的性能。

  • 在一些復(fù)雜頁面中,有些組件可能只有在用戶執(zhí)行特定操作或進(jìn)入特定頁面時才會被使用到。使用 defineAsyncComponent 可以降低初始頁面加載時的資源開銷。

  • 當(dāng)你需要動態(tài)地加載某些組件時,也可以使用 defineAsyncComponent。例如,在路由中根據(jù)不同的路徑加載不同的組件。

Vue3 之外,許多基于 Vue 3 的庫和框架也開始使用 defineAsyncComponent 來實現(xiàn)組件的異步加載。例如:

  • VitePress: Vite 的官方文檔工具,使用 defineAsyncComponent 來實現(xiàn)文檔頁面的異步加載。

  • Nuxt.js: 基于 Vue.js 的靜態(tài)網(wǎng)站生成器,從版本 2.15 開始支持 defineAsyncComponent

  • Quasar Framework: 基于 Vue.js 的 UI 框架,從版本 2.0 開始支持 defineAsyncComponent。

  • Element UI Plus: 基于 Vue 3 的 UI 庫,使用 defineAsyncComponent 來實現(xiàn)組件的異步加載。

總之,隨著 Vue 3 的普及,越來越多的庫和框架都開始使用 defineAsyncComponent 來提高應(yīng)用程序的性能。

defineCustomElement()

這個方法和 defineComponent 接受的參數(shù)相同,不同的是會返回一個原生自定義元素類的構(gòu)造器。

function defineCustomElement(
  component:
    | (ComponentOptions & { styles?: string[] })
    | ComponentOptions['setup']
): {
  new (props?: object): HTMLElement
}

除了常規(guī)的組件選項,defineCustomElement() 還支持一個特別的選項 styles,它應(yīng)該是一個內(nèi)聯(lián) CSS 字符串的數(shù)組,所提供的 CSS 會被注入到該元素的 shadow root 上。 返回值是一個可以通過 customElements.define() 注冊的自定義元素構(gòu)造器。

import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
  /* 組件選項 */
})
// 注冊自定義元素
customElements.define('my-vue-element', MyVueElement)

使用 Vue 構(gòu)建自定義元素

import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // 這里是同平常一樣的 Vue 組件選項
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement 特有的:注入進(jìn) shadow root 的 CSS
  styles: [`/* inlined css */`],
})
// 注冊自定義元素
// 注冊之后,所有此頁面中的 `<my-vue-element>` 標(biāo)簽
// 都會被升級
customElements.define('my-vue-element', MyVueElement)
// 你也可以編程式地實例化元素:
// (必須在注冊之后)
document.body.appendChild(
  new MyVueElement({
    // 初始化 props(可選)
  })
)
// 組件使用
<my-vue-element></my-vue-element>

除了 Vue 3 之外,一些基于 Vue 3 的庫和框架也開始使用 defineCustomElement 來將 Vue 組件打包成自定義元素供其他框架或純 HTML 頁面使用。例如:

  • Ionic Framework: 基于 Web Components 的移動端 UI 框架,從版本 6 開始支持使用 defineCustomElementIonic 組件打包成自定義元素。

  • LitElement: Google 推出的 Web Components 庫,提供類似 Vue 的模板語法,并支持使用 defineCustomElementLitElement 組件打包成自定義元素。

  • Stencil: 由 Ionic Team 開發(fā)的 Web Components 工具鏈,可以將任何框架的組件轉(zhuǎn)換為自定義元素,并支持使用 defineCustomElement 直接將 Vue 組件打包成自定義元素。

到此,相信大家對“Vue3中的依賴注入與組件定義怎么實現(xiàn)”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI