首頁 > web前端 > Vue.js > 主體

聊聊Vue3中的依賴注入與元件定義

青灯夜游
發布: 2023-03-21 18:46:37
轉載
1762 人瀏覽過

本次主要分享Vue3中依賴注入以及元件定義相關的幾個API,以及在常用函式庫ElementUI Plus和Vueuse中的使用情況,透過範例來理解使用場景。

聊聊Vue3中的依賴注入與元件定義

讓我們聊聊 Vue 3中依賴注入與元件定義相關的那點事兒。

provide() & inject()

#provide()

提供一個值,可以被後代元件注入。

function provide<T>(key: InjectionKey<T> | string, value: T): void
登入後複製

接收兩個參數:

    ##要注入的
  • key#,字串或Symbol;
  • export interface InjectionKey<T> extends Symbol {}
    登入後複製
    對應注入的值
與註冊生命週期鉤子的

API 類似,provide() 必須在元件的 setup() 階段同步呼叫。 【相關推薦:vuejs影片教學web前端開發

#inject()

注入一個由祖先元件或整個應用(透過

app.provide()) 提供的值。

// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined

// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

// 使用工厂函数
function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: () => T,
  treatDefaultAsFactory: true
): T
登入後複製

  • 第一個參數是注入的

    keyVue 會遍歷父元件鏈,透過符合 key 來決定所提供的值。如果父元件鏈上多個元件對同一個 key 提供了值,那麼離得更近的元件將會「覆蓋」鏈上更遠的元件所提供的值。如果沒有能透過 key 來匹配到值,inject() 將傳回 undefined,除非提供了一個預設值。

  • 第二個參數是可選的,也就是在沒有符合到

    key 時使用的預設值。它也可以是一個工廠函數,用來傳回某些創建起來比較複雜的值。如果預設值本身就是一個函數,那麼你必須將 false 作為第三個參數傳入,表示這個函數就是預設值,而不是一個工廠函數。

provide() & inject() - 官方範例
// provide
<script setup>
  import {(ref, provide)} from &#39;vue&#39; import {fooSymbol} from
  &#39;./injectionSymbols&#39; // 提供静态值 provide(&#39;foo&#39;, &#39;bar&#39;) // 提供响应式的值
  const count = ref(0) provide(&#39;count&#39;, count) // 提供时将 Symbol 作为 key
  provide(fooSymbol, count)
</script>
登入後複製
// inject
<script setup>
import { inject } from &#39;vue&#39;
import { fooSymbol } from &#39;./injectionSymbols&#39;

// 注入值的默认方式
const foo = inject(&#39;foo&#39;)

// 注入响应式的值
const count = inject(&#39;count&#39;)

// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)

// 注入一个值,若为空则使用提供的默认值
const bar = inject(&#39;foo&#39;, &#39;default value&#39;)

// 注入一个值,若为空则使用提供的工厂函数
const baz = inject(&#39;foo&#39;, () => new Map())

// 注入时为了表明提供的默认值是个函数,需要传入第三个参数
const fn = inject(&#39;function&#39;, () => {}, false)
</script>
登入後複製

provide() & inject() - ElementUI Plus 範例Breadcrumb 元件
<script setup>
import { onMounted, provide, ref } from &#39;vue&#39;
import { useNamespace } from &#39;@element-plus/hooks&#39;
import { breadcrumbKey } from &#39;./constants&#39;
import { breadcrumbProps } from &#39;./breadcrumb&#39;

defineOptions({
  name: &#39;ElBreadcrumb&#39;,
})

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

onMounted(() => {
  ......
})
</script>
登入後複製
<script setup>
import { getCurrentInstance, inject, ref, toRefs } from &#39;vue&#39;
import ElIcon from &#39;@element-plus/components/icon&#39;
import { useNamespace } from &#39;@element-plus/hooks&#39;
import { breadcrumbKey } from &#39;./constants&#39;
import { breadcrumbItemProps } from &#39;./breadcrumb-item&#39;

import type { Router } from &#39;vue-router&#39;

defineOptions({
  name: &#39;ElBreadcrumbItem&#39;,
})

const props = defineProps(breadcrumbItemProps)

const instance = getCurrentInstance()!
// 注入值
const breadcrumbContext = inject(breadcrumbKey, undefined)!
const ns = useNamespace(&#39;breadcrumb&#39;)
 ......
</script>
登入後複製

provide() & inject() - VueUse 範例

createInjectionState 原始碼 / createInjectionState 使用

package/core/computedInject 原始碼

import { type InjectionKey, inject, provide } from &#39;vue-demi&#39;

/**
 * 创建可以注入到组件中的全局状态
 */
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(&#39;InjectionState&#39;)
  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>
登入後複製

說明:當你在

Vue 中更改響應式狀態時,最終的DOM 更新並不是同步生效的,而是由Vue 將它們快取在一個佇列中,直到下一個「tick」才一起執行。這樣是為了確保每個元件無論發生多少狀態改變,都只執行一次更新。

nextTick() 可以在狀態改變後立即使用,以等待 DOM 更新完成。你可以傳遞一個回呼函數作為參數,或是 await 傳回的 Promise

nextTick() 官網範例
<script setup>
import { ref, nextTick } from &#39;vue&#39;

const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById(&#39;counter&#39;).textContent) // 0

  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById(&#39;counter&#39;).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(&#39;node&#39;)}.${ns.is(&#39;active&#39;)}`) ||
          menuElement.querySelector(`.${ns.b(&#39;node&#39;)}.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,
            })
          })
        }
      }
    }
  )
}
登入後複製

使用场景:

  • 当你需要在修改了某些数据后立即对 DOM 进行操作时,可以使用 nextTick 来确保 DOM 已经更新完毕。例如,在使用 $ref 获取元素时,需要确保元素已经被渲染才能够正确获取。

  • 在一些复杂页面中,有些组件可能会因为条件渲染或动态数据而频繁地变化。使用 nextTick 可以避免频繁地进行 DOM 操作,从而提高应用程序的性能。

  • 当需要在模板中访问某些计算属性或者监听器中的值时,也可以使用 nextTick 来确保这些值已经更新完毕。这样可以避免在视图中访问到旧值。

总之,nextTick 是一个非常有用的 API,可以确保在正确的时机对 DOM 进行操作,避免出现一些不必要的问题,并且可以提高应用程序的性能。

defineComponent()

在定义 Vue 组件时提供类型推导的辅助函数。

function defineComponent(
  component: ComponentOptions | ComponentOptions[&#39;setup&#39;]
): ComponentConstructor
登入後複製

第一个参数是一个组件选项对象。返回值将是该选项对象本身,因为该函数实际上在运行时没有任何操作,仅用于提供类型推导。

注意返回值的类型有一点特别:它会是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为了能让该返回值在 TSX 中用作标签时提供类型推导支持。

const Foo = defineComponent(/* ... */)
// 提取出一个组件的实例类型 (与其选项中的 this 的类型等价)
type FooInstance = InstanceType<typeof Foo>
登入後複製

参考:Vue3 - defineComponent 解决了什么?

defineComponent() - ElementUI Plus 示例

ConfigProvider 源码

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

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

export default ConfigProvider
登入後複製

defineComponent() - Treeshaking

因为 defineComponent() 是一个函数调用,所以它可能被某些构建工具认为会产生副作用,如 webpack。即使一个组件从未被使用,也有可能不被 tree-shake

为了告诉 webpack 这个函数调用可以被安全地 tree-shake,我们可以在函数调用之前添加一个 /_#**PURE**_/ 形式的注释:

export default /*#__PURE__*/ defineComponent(/* ... */)
登入後複製

请注意,如果你的项目中使用的是 Vite,就不需要这么做,因为 Rollup (Vite 底层使用的生产环境打包工具) 可以智能地确定 defineComponent() 实际上并没有副作用,所以无需手动注释。

defineComponent() - VueUse 示例

OnClickOutside 源码

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

      return () => {
        if (slots.default)
          return h(props.as || &#39;div&#39;, { ref: target }, slots.default())
      }
    },
  })
登入後複製

defineAsyncComponent()

定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。

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() - 官网示例

<script setup>
import { defineAsyncComponent } from &#39;vue&#39;

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    resolve(/* 从服务器获取到的组件 */)
  })
})

const AdminPage = defineAsyncComponent(() =>
  import(&#39;./components/AdminPageComponent.vue&#39;)
)
</script>
<template>
  <AsyncComp />
  <AdminPage />
</template>
登入後複製

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 ViteWebpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件。

defineAsyncComponent() - VitePress 示例

<script setup>
import { defineAsyncComponent } from &#39;vue&#39;
import type { DefaultTheme } from &#39;vitepress/theme&#39;
defineProps<{ carbonAds: DefaultTheme.CarbonAdsOptions }>()
const VPCarbonAds = __CARBON__
  ? defineAsyncComponent(() => import(&#39;./VPCarbonAds.vue&#39;))
  : () => null
</script>
<template>
  <div>
    <VPCarbonAds :carbon-ads="carbonAds" />
  </div>
</template>
登入後複製

defineAsyncComponent()使用场景:

  • 当你需要异步加载某些组件时,可以使用 defineAsyncComponent 来进行组件懒加载,这样可以提高应用程序的性能。

  • 在一些复杂页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被使用到。使用 defineAsyncComponent 可以降低初始页面加载时的资源开销。

  • 当你需要动态地加载某些组件时,也可以使用 defineAsyncComponent。例如,在路由中根据不同的路径加载不同的组件。

Vue3 之外,许多基于 Vue 3 的库和框架也开始使用 defineAsyncComponent 来实现组件的异步加载。例如:

  • VitePress: Vite 的官方文档工具,使用 defineAsyncComponent 来实现文档页面的异步加载。
  • Nuxt.js: 基于 Vue.js 的静态网站生成器,从版本 2.15 开始支持 defineAsyncComponent
  • Quasar Framework: 基于 Vue.js 的 UI 框架,从版本 2.0 开始支持 defineAsyncComponent
  • Element UI Plus: 基于 Vue 3 的 UI 库,使用 defineAsyncComponent 来实现组件的异步加载。

总之,随着 Vue 3 的普及,越来越多的库和框架都开始使用 defineAsyncComponent 来提高应用程序的性能。

defineCustomElement()

这个方法和 defineComponent 接受的参数相同,不同的是会返回一个原生自定义元素类的构造器。

function defineCustomElement(
  component:
    | (ComponentOptions & { styles?: string[] })
    | ComponentOptions[&#39;setup&#39;]
): {
  new (props?: object): HTMLElement
}
登入後複製

除了常规的组件选项,defineCustomElement() 还支持一个特别的选项 styles,它应该是一个内联 CSS 字符串的数组,所提供的 CSS 会被注入到该元素的 shadow root 上。 返回值是一个可以通过 customElements.define() 注册的自定义元素构造器。

import { defineCustomElement } from &#39;vue&#39;
const MyVueElement = defineCustomElement({
  /* 组件选项 */
})
// 注册自定义元素
customElements.define(&#39;my-vue-element&#39;, MyVueElement)
登入後複製

使用 Vue 构建自定义元素

import { defineCustomElement } from &#39;vue&#39;

const MyVueElement = defineCustomElement({
  // 这里是同平常一样的 Vue 组件选项
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`],
})
// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签
// 都会被升级
customElements.define(&#39;my-vue-element&#39;, 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 开发的 Web Components 工具链,可以将任何框架的组件转换为自定义元素,并支持使用 defineCustomElement 直接将 Vue 组件打包成自定义元素。

总之,随着 Web Components 的不断流行和发展,越来越多的库和框架都开始使用 defineCustomElement 来实现跨框架、跨平台的组件共享。

小结

本次我们围绕着 Vue3 中的依赖注入与组件定义相关的几个 API,学习其基本使用方法,并且结合着目前流行的库和框架分析了使用场景,以此来加深我们对它们的认识。

内容收录于github 仓库

(学习视频分享:vuejs入门教程编程基础视频

以上是聊聊Vue3中的依賴注入與元件定義的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板