This time we mainly share several APIs related to dependency injection and component definition in Vue3, as well as their usage in common libraries ElementUI Plus and Vueuse, and use examples to understand usage scenarios.
Let’s talk about things related to dependency injection and component definition in Vue 3
.
Provides a value that can be injected by descendant components.
function provide<T>(key: InjectionKey<T> | string, value: T): void
Receives two parameters:
key
to be injected, string or Symbol
;export interface InjectionKey<T> extends Symbol {}
is similar to the API
that registers the life cycle hook, provide()
must be in the of the component setup()
phase is called synchronously. [Related recommendations: vuejs video tutorial, web front-end development]
Inject an ancestor component or a value provided by the entire app (via 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
The first parameter is the injected key
. Vue
will traverse the parent component chain and determine the provided value by matching key
. If multiple components on the parent component chain provide a value for the same key
, then the closer component will "overwrite" the value provided by the component further up the chain. If no value can be matched by key
, inject()
will return undefined
unless a default value is provided.
The second parameter is optional, which is the default value used when no key
is matched. It can also be a factory function that returns some value that is more complex to create. If the default value itself is a function, then you must pass false
as the third parameter, indicating that this function is the default value, not a factory function.
// 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>
<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>
createInjectionState source code / createInjectionState uses
package/core/computedInject source code
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] }
Waiting for the next DOM update refresh tool method.
function nextTick(callback?: () => void): Promise<void>
Note: When you change the responsive state in Vue
, the final DOM
update does not take effect synchronously, but is done by Vue
Cache them in a queue and execute them together until the next "tick"
. This is to ensure that each component only performs one update regardless of how many state changes occur.
nextTick()
can be used immediately after the status changes to wait for the DOM
update to complete. You can pass a callback function as a parameter, or a Promise returned by await.
<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>
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) } }) } ...... })
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
进行操作,避免出现一些不必要的问题,并且可以提高应用程序的性能。
在定义 Vue
组件时提供类型推导的辅助函数。
function defineComponent( component: ComponentOptions | ComponentOptions['setup'] ): ComponentConstructor
第一个参数是一个组件选项对象。返回值将是该选项对象本身,因为该函数实际上在运行时没有任何操作,仅用于提供类型推导。
注意返回值的类型有一点特别:它会是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为了能让该返回值在 TSX
中用作标签时提供类型推导支持。
const Foo = defineComponent(/* ... */) // 提取出一个组件的实例类型 (与其选项中的 this 的类型等价) type FooInstance = InstanceType<typeof Foo>
参考:Vue3 - defineComponent 解决了什么?
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()
是一个函数调用,所以它可能被某些构建工具认为会产生副作用,如 webpack
。即使一个组件从未被使用,也有可能不被 tree-shake
。
为了告诉 webpack
这个函数调用可以被安全地 tree-shake
,我们可以在函数调用之前添加一个 /_#**PURE**_/
形式的注释:
export default /*#__PURE__*/ defineComponent(/* ... */)
请注意,如果你的项目中使用的是 Vite
,就不需要这么做,因为 Rollup
(Vite
底层使用的生产环境打包工具) 可以智能地确定 defineComponent()
实际上并没有副作用,所以无需手动注释。
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()) } }, })
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
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 }
<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
搭配使用。类似 Vite
和 Webpack
这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue
单文件组件。
<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()
使用场景:
当你需要异步加载某些组件时,可以使用 defineAsyncComponent
来进行组件懒加载,这样可以提高应用程序的性能。
在一些复杂页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被使用到。使用 defineAsyncComponent
可以降低初始页面加载时的资源开销。
当你需要动态地加载某些组件时,也可以使用 defineAsyncComponent
。例如,在路由中根据不同的路径加载不同的组件。
除 Vue3
之外,许多基于 Vue 3
的库和框架也开始使用 defineAsyncComponent
来实现组件的异步加载。例如:
Vite
的官方文档工具,使用 defineAsyncComponent
来实现文档页面的异步加载。defineAsyncComponent
。Vue.js
的 UI 框架,从版本 2.0 开始支持 defineAsyncComponent
。Vue 3
的 UI 库,使用 defineAsyncComponent
来实现组件的异步加载。总之,随着 Vue 3 的普及,越来越多的库和框架都开始使用 defineAsyncComponent
来提高应用程序的性能。
这个方法和 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)
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
的移动端 UI 框架,从版本 6 开始支持使用 defineCustomElement
将 Ionic
组件打包成自定义元素。Web Components
库,提供类似 Vue
的模板语法,并支持使用 defineCustomElement
将 LitElement
组件打包成自定义元素。Ionic Team
开发的 Web Components
工具链,可以将任何框架的组件转换为自定义元素,并支持使用 defineCustomElement
直接将 Vue
组件打包成自定义元素。总之,随着 Web Components
的不断流行和发展,越来越多的库和框架都开始使用 defineCustomElement
来实现跨框架、跨平台的组件共享。
本次我们围绕着 Vue3
中的依赖注入与组件定义相关的几个 API,学习其基本使用方法,并且结合着目前流行的库和框架分析了使用场景,以此来加深我们对它们的认识。
内容收录于github 仓库
The above is the detailed content of Let's talk about dependency injection and component definition in Vue3. For more information, please follow other related articles on the PHP Chinese website!