• 技术文章 >web前端 >Vue.js

    聊聊Vue3中的依赖注入与组件定义

    青灯夜游青灯夜游2023-03-21 18:46:37转载302

    本次主要分享Vue3中依赖注入以及组件定义相关的几个API,以及在常用库ElementUI Plus和Vueuse中的使用情况,通过示例来理解使用场景。

    让我们聊聊 Vue 3中依赖注入与组件定义相关的那点事儿。

    provide() & inject()

    provide()

    提供一个值,可以被后代组件注入。

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

    接收两个参数:

    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

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

    // provide
    <script setup>
      import {(ref, provide)} from 'vue' import {fooSymbol} from
      './injectionSymbols' // 提供静态值 provide('foo', 'bar') // 提供响应式的值
      const count = ref(0) provide('count', count) // 提供时将 Symbol 作为 key
      provide(fooSymbol, count)
    </script>
    // inject
    <script setup>
    import { inject } from 'vue'
    import { fooSymbol } from './injectionSymbols'
    
    // 注入值的默认方式
    const foo = inject('foo')
    
    // 注入响应式的值
    const count = inject('count')
    
    // 通过 Symbol 类型的 key 注入
    const foo2 = inject(fooSymbol)
    
    // 注入一个值,若为空则使用提供的默认值
    const bar = inject('foo', 'default value')
    
    // 注入一个值,若为空则使用提供的工厂函数
    const baz = inject('foo', () => new Map())
    
    // 注入时为了表明提供的默认值是个函数,需要传入第三个参数
    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'
    
    /**
     * 创建可以注入到组件中的全局状态
     */
    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>

    说明:当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。

    nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise

    nextTick() 官网示例

    <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 此时已经更新
      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,
                })
              })
            }
          }
        }
      )
    }

    使用场景:

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

    defineComponent()

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

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

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

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

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

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

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

    请注意,如果你的项目中使用的是 Vite,就不需要这么做,因为 Rollup (Vite 底层使用的生产环境打包工具) 可以智能地确定 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()

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

    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 'vue'
    
    const AsyncComp = defineAsyncComponent(() => {
      return new Promise((resolve, reject) => {
        resolve(/* 从服务器获取到的组件 */)
      })
    })
    
    const AdminPage = defineAsyncComponent(() =>
      import('./components/AdminPageComponent.vue')
    )
    </script>
    <template>
      <AsyncComp />
      <AdminPage />
    </template>

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

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

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

    defineCustomElement()

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

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

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

    import { defineCustomElement } from 'vue'
    const MyVueElement = defineCustomElement({
      /* 组件选项 */
    })
    // 注册自定义元素
    customElements.define('my-vue-element', MyVueElement)

    使用 Vue 构建自定义元素

    import { defineCustomElement } from 'vue'
    
    const MyVueElement = defineCustomElement({
      // 这里是同平常一样的 Vue 组件选项
      props: {},
      emits: {},
      template: `...`,
      // defineCustomElement 特有的:注入进 shadow root 的 CSS
      styles: [`/* inlined css */`],
    })
    // 注册自定义元素
    // 注册之后,所有此页面中的 `<my-vue-element>` 标签
    // 都会被升级
    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 页面使用。例如:

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

    小结

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

    内容收录于github 仓库

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

    以上就是聊聊Vue3中的依赖注入与组件定义的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:Vue vue3
    上一篇:深入了解vuex的实现原理 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • 分享一个VUE页面声音+标题闪烁通知的组件• vue优先级比较:为什么v-for比v-if高• Vue3响应式函数对比:toRef() vs toRefs()• 快速搞懂Vue2 diff算法(图文详解)• 深入了解vuex的实现原理
    1/1

    PHP中文网