Let’s talk Vue 3
about dependency injection and component definition.
Contents
main content
In this sharing, we mainly cover the following contents:
- 📝 provide() & inject() – dependency injection
- 🛠 nextTick() – after the next DOM update cycle
- 🎨Component definition
- defineComponent() – component definition type inference helper function
- defineAsyncComponent() – asynchronous component
- defineCustomElement() – Constructor for native custom element classes
provide() & inject()
provide()
Provides a value that can be injected by descendant components.
function provide<T>( key : InjectionKey <T> | string , value : T): void
Receives two parameters:
- To be injected
key
, string orSymbol
;
export interface InjectionKey <T> extends Symbol {}
- Corresponding to the value injected
Similar to registering life cycle hooks API
, provide()
they must be called synchronously in the component’s setup()
phase.
inject()
app.provide()
Inject a value provided by an ancestor component or the entire application (via ).
// No default value function inject<T>( key : InjectionKey <T> | string ): T | undefined //With default value function inject<T>( key : InjectionKey <T> | string , defaultValue : T): T // Use factory function function inject<T>( key : InjectionKey <T> | string , defaultValue : () => T, treatDefaultAsFactory : true ): T
- The first parameter is injected
key
.Vue
The parent component chain is traversed tokey
determine the provided value by matching . If multiple components in the parent component chain provide a value for the samekey
, then the closer component will “overwrite” the value provided by the component further up the chain. If nokey
value is matched by ,inject()
it will be returnedundefined
unless a default value is provided. - The second parameter is optional and is
key
the default value used if no match is found. 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 mustfalse
pass it in as the third parameter, indicating that this function is the default value, not a factory function.
provide() & inject() – official example
//provide <script setup> import {(ref, provide)} from 'vue' import {fooSymbol} from './injectionSymbols' // Provide static values provide('foo', 'bar') // Provide responsive values const count = ref ( 0 ) provide ( 'count' , count) // Use Symbol as key when providing provide (fooSymbol, count) </script>
//inject <script setup> import { inject } from 'vue' import { fooSymbol } from './injectionSymbols' //The default way to inject values is const foo = inject ( 'foo' ) //Inject the response value const count = inject ( 'count' ) // Inject const foo2 = inject (fooSymbol) through the key of Symbol type // Inject a value, if empty, use the provided default value const bar = inject ( 'foo' , 'default value' ) // Inject a value, if it is empty, use the provided factory function const baz = inject ( 'foo' , () => new Map ()) // When injecting, in order to indicate that the default value provided is a function, the third parameter const fn = inject ( 'function' , () => {}, false ) needs to be passed in </script>
provide() & inject() – ElementUI Plus sample Breadcrumb component
<script lang= "ts" 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 value provide (breadcrumbKey, props) onMounted ( () => { ... }) </script>
<script lang= "ts" 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 ()! // Inject value const breadcrumbContext = inject (breadcrumbKey, undefined )! const ns = useNamespace ( 'breadcrumb' ) ... </script>
provide() & inject() – VueUse example
createInjectionState source code / createInjectionState usage
package/core/computedInject source code
import { type InjectionKey , inject, provide } from 'vue-demi' /** * Create global state that can be injected into components */ 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()
Utility method that waits for the next DOM update to be refreshed.
function nextTick ( callback?: () => void ): Promise < void >
Note: When you Vue
change reactive state in , the final DOM
updates are not synchronized, but are Vue
cached in a queue until the next one “tick”
is executed together. This is to ensure that each component only performs one update regardless of how many state changes occur.
nextTick()
Can be used immediately after a status change to wait for DOM
the update to complete. You can pass a callback function as a parameter, or await the Promise returned .
nextTick() official website example
<script setup> import { ref, nextTick } from 'vue' const count = ref ( 0 ) async function increment () { count.value ++ // DOM has not been updated console . log ( document . getElementById ( 'counter' ) . textContent ) // 0 await nextTick () // DOM has been updated at this time console . log ( document . getElementById ( 'counter' ). textContent ) // 1 } </script> < template > < button id = "counter" @ click = "increment" > {{ count }} </ button > </ template >
nextTick() – ElementUI Plus example
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 example
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 , }) }) } } } ) }
scenes to be used:
DOM
When you need to operate immediately after modifying some data , you can usenextTick
to ensure thatDOM
has been updated. For example, when using$ref
to obtain an element, you need to ensure that the element has been rendered before it can be obtained correctly.- In some complex pages, some components may change frequently due to conditional rendering or dynamic data. Using can improve application performance by avoiding
nextTick
frequent operations.DOM
- When you need to access certain calculated properties or values in the listener in the template, you can also use
nextTick
to ensure that these values have been updated. This avoids accessing old values in the view.
In short, nextTick
it is a very useful API that can ensure that DOM
operations are performed at the right time, avoid unnecessary problems, and improve application performance.
defineComponent()
Vue
Provides helper functions for type inference when defining components.
function defineComponent ( component: ComponentOptions | ComponentOptions[ 'setup' ] ): ComponentConstructor
The first parameter is a component options object. The return value will be the options object itself, since the function actually does nothing at runtime and is only used to provide type deduction.
Note that the type of the return value is a little special: it will be a constructor type, and its instance type is the component instance type inferred based on the options. This is TSX
to provide type inference support when the return value is used as a label in .
const Foo = defineComponent ( /* ... */ ) // Extract the instance type of a component (equivalent to the type of this in its options) type FooInstance = InstanceType < typeof Foo >
Reference: Vue3 – What does defineComponent solve?
defineComponent() – ElementUI Plus example
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
Because defineComponent()
is a function call, it may be considered to have side effects by some build tools, webpack
e.g. Even if a component is never used, it may not be tree-shake
.
To tell that webpack
this function call can be made safely tree-shake
, we can add a comment of the form before the function call /_#**PURE**_/
:
export default /*#__PURE__*/ defineComponent ( /* ... */ )
Please note that if you are using it in your project Vite
, you don’t need to do this, because Rollup
( Vite
the underlying production environment packaging tool used) can intelligently determine that defineComponent()
there are actually no side effects, so there is no need to manually annotate it.
defineComponent() – VueUse example
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()
Define an asynchronous component that is loaded lazily at runtime. The argument can be an asynchronous loading function, or an options object that more specifically customizes the loading behavior.
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() – official website example
< script setup > import { defineAsyncComponent } from 'vue' const AsyncComp = defineAsyncComponent ( () => { return new Promise ( ( resolve, reject ) => { resolve ( /* component obtained from the server */ ) }) }) const AdminPage = defineAsyncComponent ( () => import ( './components/AdminPageComponent.vue' ) ) </ script > < template > < AsyncComp /> < AdminPage /> </ template >
ES
Module dynamic import will also return one Promise
, so in most cases we will defineAsyncComponent
use it with . Build tools like Vite
and Webpack
also support this syntax (and will use them as code splitting points when packaging), so we can also use it to import Vue
single-file components.
defineAsyncComponent() – VitePress Example
<script setup lang= "ts" > 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 class = "VPDocAsideCarbonAds" > < VPCarbonAds :carbon-ads = "carbonAds" /> </ div > </ template >
defineAsyncComponent()
scenes to be used:
- When you need to load certain components asynchronously, you can use
defineAsyncComponent
to lazy load components, which can improve application performance. - In some complex pages, some components may only be used when the user performs a specific operation or enters a specific page. Use
defineAsyncComponent
can reduce resource overhead during initial page load. - It can also be used when you need to load certain components dynamically
defineAsyncComponent
. For example, loading different components based on different paths in routing.
In Vue3
addition to, many Vue 3
libraries and frameworks based on have also begun to use defineAsyncComponent
to implement asynchronous loading of components. For example:
- VitePress :
Vite
The official documentation tool, usesdefineAsyncComponent
to implement asynchronous loading of documentation pages. - Nuxt.js : Static website generator based on Vue.js, supported starting from version 2.15
defineAsyncComponent
. - Quasar Framework :
Vue.js
UI framework based on Quasar, supported starting from version 2.0defineAsyncComponent
. - Element UI Plus :
Vue 3
A UI library based on , which usesdefineAsyncComponent
to implement asynchronous loading of components.
In short, with the popularity of Vue 3, more and more libraries and frameworks are beginning to use it defineAsyncComponent
to improve application performance.
defineCustomElement()
This method defineComponent
accepts the same parameters as , except that it returns a constructor of a native custom element class .
function defineCustomElement ( component: | (ComponentOptions & { styles?: string [] }) | ComponentOptions[ 'setup' ] ): { new (props?: object ): HTMLElement }
In addition to the regular component options, defineCustomElement()
a special option is supported styles
, which should be an array of inline CSS
strings, and the provided ones CSS
will be injected into the element shadow root
.
The return value is a customElements.define()
custom element constructor that can be registered via .
import { defineCustomElement } from 'vue' const MyVueElement = defineCustomElement ({ /* component options*/ }) // Register custom elements customElements. define ( 'my-vue-element' , MyVueElement )
Build custom elements using Vue
import { defineCustomElement } from 'vue' const MyVueElement = defineCustomElement ({ // Here are the usual Vue component options props : {}, emits : {}, template : `...` , // defineCustomElement-specific: CSS styles injected into the shadow root : [ `/* inlined css */` ], }) // Register custom elements // After registration, all `<my-vue-element>` tags in this page // will be upgraded customElements. define ( 'my-vue-element' , MyVueElement ) // You can also Programmatically instantiate elements: // (must be after registration) document . body . appendChild ( new MyVueElement ({ // Initialize props (optional) }) ) // Component uses <my-vue-element></my-vue-element>
In addition Vue 3
to , some Vue 3
libraries and frameworks based on have also begun to use defineCustomElement
to Vue
package components into custom elements for use by other frameworks or pure HTML pages. For example:
- Ionic Framework :
Web Components
A mobile UI framework based on Ionic. Starting from version 6, it supportsdefineCustomElement
packagingIonic
components into custom elements. - LitElement : A library launched by Google
Web Components
that providesVue
template syntax similar to and supportsdefineCustomElement
packagingLitElement
components into custom elements. - Stencil : A tool chain
Ionic Team
developed by StencilWeb Components
that can convert components of any framework into custom elements and supportsdefineCustomElement
directVue
packaging of components into custom elements.
In short, with Web Components
the continuous popularity and development of , more and more libraries and frameworks are beginning to use defineCustomElement
to achieve cross-framework and cross-platform component sharing.
summary
This time we focus on Vue3
several APIs related to dependency injection and component definition in , learn their basic usage, and analyze usage scenarios combined with currently popular libraries and frameworks to deepen our understanding of them.
The content is included in the github repository