Let’s talk Vue 3about 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.VueThe parent component chain is traversed tokeydetermine 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 nokeyvalue is matched by ,inject()it will be returnedundefinedunless a default value is provided. - The second parameter is optional and is
keythe 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 mustfalsepass 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 Vuechange reactive state in , the final DOMupdates are not synchronized, but are Vuecached 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 DOMthe 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:
DOMWhen you need to operate immediately after modifying some data , you can usenextTickto ensure thatDOMhas been updated. For example, when using$refto 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
nextTickfrequent operations.DOM - When you need to access certain calculated properties or values in the listener in the template, you can also use
nextTickto ensure that these values have been updated. This avoids accessing old values in the view.
In short, nextTickit is a very useful API that can ensure that DOMoperations are performed at the right time, avoid unnecessary problems, and improve application performance.
defineComponent()
VueProvides 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 TSXto 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, webpacke.g. Even if a component is never used, it may not be tree-shake.
To tell that webpackthis 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( Vitethe 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 >
ESModule dynamic import will also return one Promise, so in most cases we will defineAsyncComponentuse it with . Build tools like Viteand Webpackalso support this syntax (and will use them as code splitting points when packaging), so we can also use it to import Vuesingle-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
defineAsyncComponentto 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
defineAsyncComponentcan 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 Vue3addition to, many Vue 3libraries and frameworks based on have also begun to use defineAsyncComponentto implement asynchronous loading of components. For example:
- VitePress :
ViteThe official documentation tool, usesdefineAsyncComponentto 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.jsUI framework based on Quasar, supported starting from version 2.0defineAsyncComponent. - Element UI Plus :
Vue 3A UI library based on , which usesdefineAsyncComponentto implement asynchronous loading of components.
In short, with the popularity of Vue 3, more and more libraries and frameworks are beginning to use it defineAsyncComponentto improve application performance.
defineCustomElement()
This method defineComponentaccepts 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 CSSstrings, and the provided ones CSSwill 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 3to , some Vue 3libraries and frameworks based on have also begun to use defineCustomElementto Vuepackage components into custom elements for use by other frameworks or pure HTML pages. For example:
- Ionic Framework :
Web ComponentsA mobile UI framework based on Ionic. Starting from version 6, it supportsdefineCustomElementpackagingIoniccomponents into custom elements. - LitElement : A library launched by Google
Web Componentsthat providesVuetemplate syntax similar to and supportsdefineCustomElementpackagingLitElementcomponents into custom elements. - Stencil : A tool chain
Ionic Teamdeveloped by StencilWeb Componentsthat can convert components of any framework into custom elements and supportsdefineCustomElementdirectVuepackaging of components into custom elements.
In short, with Web Componentsthe continuous popularity and development of , more and more libraries and frameworks are beginning to use defineCustomElementto achieve cross-framework and cross-platform component sharing.
summary
This time we focus on Vue3several 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
