Vue 3中依赖注入与组件定义相关的那点事儿
创始人
2025-05-29 14:57:24
0

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

主要内容

本次分享,我们主要涉及以下内容:

  • 📝 provide() & inject() - 依赖注入
  • 🛠 nextTick() - 下一个 DOM 更新周期后
  • 🎨 组件定义
    • defineComponent() - 组件定义类型推导辅助函数
    • defineAsyncComponent() - 异步组件
    • defineCustomElement() - 原生自定义元素类的构造器

provide() & inject()

provide()

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

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

接收两个参数:

  • 要注入的 key,字符串或者 Symbol
export interface InjectionKey extends Symbol {}
  • 对应注入的值

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

// 没有默认值
function inject(key: InjectionKey | string): T | undefined// 带有默认值
function inject(key: InjectionKey | string, defaultValue: T): T// 使用工厂函数
function inject(key: InjectionKey | string,defaultValue: () => T,treatDefaultAsFactory: true
): T
  • 第一个参数是注入的 keyVue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

  • 第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

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

// provide

// inject

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



provide() & inject() - VueUse 示例

createInjectionState 源码 / createInjectionState 使用

package/core/computedInject 源码

import { type InjectionKey, inject, provide } from 'vue-demi'/*** 创建可以注入到组件中的全局状态*/
export function createInjectionState, Return>(composable: (...args: Arguments) => Return
): readonly [useProvidingState: (...args: Arguments) => Return,useInjectedState: () => Return | undefined
] {const key: string | InjectionKey = 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

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

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

nextTick() 官网示例


nextTick() - ElementUI Plus 示例

ElCascaderPanel 源码

export default defineComponent({......const syncMenuState = (newCheckedNodes: CascaderNode[],reserveExpandingState = true) => {......checkedNodes.value = newNodesnextTick(scrollToExpandingNode)}const scrollToExpandingNode = () => {if (!isClient) returnmenuList.value.forEach((menu) => {const menuElement = menu?.$elif (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......
) {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,})})}}})
}

使用场景:

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

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

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

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

defineComponent()

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

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

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

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

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

参考: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 = InstanceTypeexport 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({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
interface AsyncComponentOptions {loader: AsyncComponentLoaderloadingComponent?: ComponenterrorComponent?: Componentdelay?: numbertimeout?: numbersuspensible?: booleanonError?: (error: Error,retry: () => void,fail: () => void,attempts: number) => any
}

defineAsyncComponent() - 官网示例



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

defineAsyncComponent() - VitePress 示例



defineAsyncComponent()使用场景:

  1. 当你需要异步加载某些组件时,可以使用 defineAsyncComponent 来进行组件懒加载,这样可以提高应用程序的性能。
  2. 在一些复杂页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被使用到。使用 defineAsyncComponent 可以降低初始页面加载时的资源开销。
  3. 当你需要动态地加载某些组件时,也可以使用 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['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 的 CSSstyles: [`/* inlined css */`],
})
// 注册自定义元素
// 注册之后,所有此页面中的 `` 标签
// 都会被升级
customElements.define('my-vue-element', MyVueElement)
// 你也可以编程式地实例化元素:
// (必须在注册之后)
document.body.appendChild(new MyVueElement({// 初始化 props(可选)})
)
// 组件使用

除了 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 仓库

相关内容

热门资讯

Python 学习 Python环境搭建conda 基础命令python 基础命令if __name__ =&#...
精选关于大学生职业规划范文 大...   大学生职业规划  【1】大学生职业生涯规划书范文  一、前言  俗话说:"志不立,天下无可成之事...
关于大学生职业生涯规划范文 数...   大学生职业生涯规划  【1】  一、自我认知  通过人才测评分析结果以及本人对自己的认识、朋友对...
护理职业生涯规划书范文 大专护...   护理职业生涯规划书范文  【1】护理系大学生职业生涯规划书范文  前言  在今天这个人才竞争的时...
大学生职业规划书范文【推荐】 ... 一 个人概况  姓名:张银平 性别: 女  出生年月:1989年12月5日 健康状况: 良好  毕业...
会计职业生涯规划书模板精选 会... 【1】会计职业生涯规划书范文  人生因为有梦想而精彩,一个人有梦想并且不懈的追求的人,才能有亮丽而耀...
Redis 内存管理(淘汰策略... 目录Redis 内存管理(淘汰策略/删除策略)一、淘汰算法1、LRU算法...
用问答的方式学习Require 我需要在vue3中引入require,该怎么导入? 在 Vue 3 中...
关于会计职业生涯规划书模板 会... 篇一:会计职业生涯规划模板  今天站在哪里并不重要,最重要的是你下一步迈向哪里。职业生涯规划对于每个...
护理职业生涯规划书模板 护理职... 篇一:护士的职业生涯规划书范文  时间: (****年-****年,**岁至**岁)  美好愿望:工...
工商管理专业职业生涯规划书 工...  工商管理专业职业生涯规划书(一)  一、自我分析  我是一个真诚的人。为什么我会把真诚写在第一位?...
大学生职业生涯规划书精选 计算... 【1】大学生职业生涯规划书范文  第一章:总论(引言)实现梦想的第一步就是要有梦想。没有梦想对于每个...
药学专业职业生涯规划书 大专药...  药学专业职业生涯规划书(一)  岁月流光,转瞬即逝,我将迈入大一的生活中去。迈入新的一年里,应该有...
【TypeScript 入门】... 前两个章节都是介绍 TypeScript 中最基础的语法和特性的,这一章,我们讲 TypeScrip...
语义分割网络论文集锦 年份会议/期刊标题内容架构图2015CVPRFully Convolutional Networks...
数学系职业生涯规划书 数学类职...   数学系职业生涯规划书(一)  数学是解决问题的语言,是所有科学和技术的核心。接受数学教育的优点是...
国贸系职业生涯规划书 国贸专业...   国贸系职业生涯规划书(一)  一、前言:职业规划意义  每一个人都拥有自己的梦想,然而并非每一个...
哲学系职业生涯规划书范文 分析...  哲学系职业生涯规划书(一)  一、前言  未来,掌握在自己手中。踏着时光车轮,我已走到23岁的年轮...
法学系职业生涯规划书 法学职业...   法学系职业生涯规划书(一)  一、前言  从我们走入大学校门到现在已经快要一年了,现在我们的大一...
物流管理专业职业生涯规划书 物... 第一部分 物流管理师职业前景分析  我国物流产业兴起于上世纪 90年代,起步虽晚,但发展势头强劲,到...