让咱们聊聊 Vue 3
中依赖注入与组件定义相干的那点事儿。
次要内容
本次分享,咱们次要波及以下内容:
- provide() & inject() - 依赖注入
- nextTick() - 下一个 DOM 更新周期后
组件定义
- defineComponent() - 组件定义类型推导辅助函数
- defineAsyncComponent() - 异步组件
- defineCustomElement() - 原生自定义元素类的结构器
provide() & inject()
provide()
提供一个值,能够被后辈组件注入。
function provide<T>(key: InjectionKey<T> | string, value: T): void
接管两个参数:
- 要注入的
key
,字符串或者Symbol
;
export interface InjectionKey<T> extends Symbol {}
- 对应注入的值
与注册生命周期钩子的 API
相似,provide()
必须在组件的 setup()
阶段同步调用。
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
- 第一个参数是注入的
key
。Vue
会遍历父组件链,通过匹配key
来确定所提供的值。如果父组件链上多个组件对同一个key
提供了值,那么离得更近的组件将会“笼罩”链上更远的组件所提供的值。如果没有能通过key
匹配到值,inject()
将返回undefined
,除非提供了一个默认值。 - 第二个参数是可选的,即在没有匹配到
key
时应用的默认值。它也能够是一个工厂函数,用来返回某些创立起来比较复杂的值。如果默认值自身就是一个函数,那么你必须将false
作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
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 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(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()!// 注入值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, }) }) } } } )}
应用场景:
- 当你须要在批改了某些数据后立刻对
DOM
进行操作时,能够应用nextTick
来确保DOM
曾经更新结束。例如,在应用$ref
获取元素时,须要确保元素曾经被渲染才可能正确获取。 - 在一些简单页面中,有些组件可能会因为条件渲染或动态数据而频繁地变动。应用
nextTick
能够防止频繁地进行DOM
操作,从而进步应用程序的性能。 - 当须要在模板中拜访某些计算属性或者监听器中的值时,也能够应用
nextTick
来确保这些值曾经更新结束。这样能够防止在视图中拜访到旧值。
总之,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): Componenttype 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
搭配应用。相似 Vite
和 Webpack
这样的构建工具也反对此语法 (并且会将它们作为打包时的代码宰割点),因而咱们也能够用它来导入 Vue
单文件组件。
defineAsyncComponent() - VitePress 示例
<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()
应用场景:
- 当你须要异步加载某些组件时,能够应用
defineAsyncComponent
来进行组件懒加载,这样能够进步应用程序的性能。 - 在一些简单页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被应用到。应用
defineAsyncComponent
能够升高初始页面加载时的资源开销。 - 当你须要动静地加载某些组件时,也能够应用
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 的 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 页面应用。例如:
- Ionic Framework: 基于
Web Components
的挪动端 UI 框架,从版本 6 开始反对应用defineCustomElement
将Ionic
组件打包成自定义元素。 - LitElement: Google 推出的
Web Components
库,提供相似Vue
的模板语法,并反对应用defineCustomElement
将LitElement
组件打包成自定义元素。 - Stencil: 由
Ionic Team
开发的Web Components
工具链,能够将任何框架的组件转换为自定义元素,并反对应用defineCustomElement
间接将Vue
组件打包成自定义元素。
总之,随着 Web Components
的一直风行和倒退,越来越多的库和框架都开始应用 defineCustomElement
来实现跨框架、跨平台的组件共享。
小结
本次咱们围绕着 Vue3
中的依赖注入与组件定义相干的几个 API,学习其根本应用办法,并且联合着目前风行的库和框架剖析了应用场景,以此来加深咱们对它们的意识。
内容收录于github 仓库