Vue3.0进阶之依赖注入探秘

本文转载自微信公众号「全栈修仙之路」,作者阿宝哥。转载本文请联系全栈修仙之路公众号。  

创新互联建站服务项目包括麦积网站建设、麦积网站制作、麦积网页制作以及麦积网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,麦积网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到麦积省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

本文是 Vue 3.0 进阶系列 的第六篇文章,在这篇文章中,阿宝哥将带大家一起探索 Vue 3 中的依赖注入功能。

使用过 Angular 的小伙伴对 依赖注入 应该不会陌生,依赖注入简称为 DI(Dependency Injection)。组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。

在 Vue 3.0 中,为我们提供了简单的依赖注入功能 —— provide/inject。它们解决了以下问题:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,这样使用起来会很麻烦。

为了解决上述问题,Vue 提供了 provide 和 inject。使用 provide/inject 之后,无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。

(图片来源 —— https://v3.cn.vuejs.org/guide/component-provide-inject.html)

由上图可知,在父组件上通过 provide 提供数据,子组件通过 inject 注入数据。介绍完 provide/inject 的作用之后,我们来看一下具体的示例。

一、Provide/Inject 使用示例

在使用 provide/inject 特性时,你可以通过 provide 和 inject 选项的方式来使用它。这种方式官方文档已经有介绍了,这里阿宝哥将介绍另一种使用方式,即在组合式 API 的 setup 组件选项中,通过 provide 和 inject 函数的方式来实现依赖注入。

 
 
 
 
 
  •  
  • 在以上示例中,在 Provider 组件内通过 provide 函数配置了数据,而在 Consumer 组件中通过 inject 函数获取 Provider 组件中已配置的数据。需要注意的是,示例中的 Consumer 组件是作为 Provider 组件的孙组件。因此,通过使用 provide/inject 提供的依赖注入功能,我们实现了数据的跨层级传递。

    介绍完 provide 和 inject 函数的基本使用之后,接下来阿宝哥将带大家一起来揭开它们背后的秘密。

    二、Provide 函数

    在分析 provide 函数之前,我们先来回顾一下它的用法:

     
     
     
     
    1. const Provider = { 
    2.   setup() { 
    3.     provide('name', '阿宝哥') 
    4.     return () => h(Middle) 
    5.   } 

    该函数被定义在 runtime-core/src/apiInject.ts 文件中:

     
     
     
     
    1. // packages/runtime-core/src/apiInject.ts 
    2. export interface InjectionKey extends Symbol {} 
    3.  
    4. export function provide(key: InjectionKey | string | number, value: T) { 
    5.   if (!currentInstance) { 
    6.     if (__DEV__) { 
    7.       warn(`provide() can only be used inside setup().`) 
    8.     } 
    9.   } else { 
    10.     let provides = currentInstance.provides 
    11.     // 默认情况下,组件实例会继承于它父组件实例的 provides 对象,当它需要为本身提供 
    12.     // 值是,它将使用父组件的 provides 对象作为原型对象来创建属于它自己的 provides 
    13.     // 对象。这样的话,`inject` 函数就可以简单地从直接父对象中查找需注入的值,并让原型链 
    14.     // 来完成这个工作。 
    15.     const parentProvides = 
    16.       currentInstance.parent && currentInstance.parent.provides 
    17.     if (parentProvides === provides) { 
    18.       provides = currentInstance.provides = Object.create(parentProvides) 
    19.     } 
    20.     // TS 不允许使用 symbol 作为索引类型 
    21.     provides[key as string] = value 
    22.   } 

    通过观察以上的代码,我们可以得出以下结论:

    在以上代码中,我们见到了 currentInstance 对象,那么这个对象内部的结构是怎样的呢?为了一探究竟,我们在 provide 函数内部加个断点:

    由上图可知, currentInstance 是一个含有多种属性的普通对象。其中 bc(BEFORE_CREATE)、bm(BEFORE_MOUNT) 和 bu(BEFORE_UPDATE) 是与生命周期相关的钩子。那么问题又来了,currentInstance 对象是怎么创建的?看过之前 Vue 3.0 进阶系列文章的小伙伴,可能对 createComponentInstance 函数有点印象,该函数的作用就是创建组件实例,具体代码如下所示:

     
     
     
     
    1. // packages/runtime-core/src/component.ts 
    2. export function createComponentInstance( 
    3.   vnode: VNode, 
    4.   parent: ComponentInternalInstance | null, 
    5.   suspense: SuspenseBoundary | null 
    6. ) { 
    7.   const type = vnode.type as ConcreteComponent 
    8.   // inherit parent app context - or - if root, adopt from root vnode 
    9.   const appContext = 
    10.     (parent ? parent.appContext : vnode.appContext) || emptyAppContext 
    11.  
    12.   const instance: ComponentInternalInstance = { 
    13.     uid: uid++, vnode, type, 
    14.     parent, appContext, 
    15.     root: null!, // to be immediately set 
    16.     subTree: null!, // will be set synchronously right after creation 
    17.     update: null!, // will be set synchronously right after creation 
    18.     render: null, effects: null, 
    19.     provides: parent ? parent.provides : Object.create(appContext.provides), 
    20.     bc, bm, bu 
    21.     // 省略大部分属性 
    22.   } 
    23.   // 省略部分代码 
    24.   instance.root = parent ? parent.root : instance 
    25.   instance.emit = emit.bind(null, instance) 
    26.   return instance 

    需要注意的是,如果当前组件 parent 属性的值不为 null 时,则将使用 parent.provides 的值作为组件实例 provides 属性的属性值。介绍完 provide 和 createComponentInstance 函数,为了让大家能够更好地理解前面的示例,阿宝哥用一张图来总结一下示例中组件之间的关系:

    对于根组件来说,它的 parent 属性值为 null。好的,provide 函数就先介绍到这里,下面我们来开始介绍 inject 函数。

    三、Inject 函数

    同样,在分析 inject 函数之前,我们也先来回顾一下它的用法:

     
     
     
     
    1. const Consumer = { 
    2.   setup() { 
    3.     const name = inject('name') 
    4.     return () => `大家好,我是${name}!` 
    5.   } 

    inject 函数与 provide 函数是互相配合的,它们都被定义在 runtime-core/src/apiInject.ts 文件中:

     
     
     
     
    1. // packages/runtime-core/src/apiInject.ts 
    2. export function inject( 
    3.   key: InjectionKey | string, 
    4.   defaultValue?: unknown, 
    5.   treatDefaultAsFactory = false 
    6. ) { 
    7.   const instance = currentInstance || currentRenderingInstance // 获取当前实例 
    8.   if (instance) { 
    9.     // to support `app.use` plugins, 
    10.     // fallback to appContext's `provides` if the intance is at root 
    11.     const provides = 
    12.       instance.parent == null 
    13.         ? instance.vnode.appContext && instance.vnode.appContext.provides 
    14.         : instance.parent.provides 
    15.  
    16.     if (provides && (key as string | symbol) in provides) { 
    17.       // TS doesn't allow symbol as index type 
    18.       return provides[key as string] 
    19.     } else if (arguments.length > 1) { 
    20.       return treatDefaultAsFactory && isFunction(defaultValue) 
    21.         ? defaultValue() 
    22.         : defaultValue 
    23.     } else if (__DEV__) { 
    24.       warn(`injection "${String(key)}" not found.`) 
    25.     } 
    26.   } else if (__DEV__) { 
    27.     warn(`inject() can only be used inside setup() or functional components.`) 
    28.   } 

    在 inject 函数中,我们可以清楚地看到如果当前实例的 parent 属性为 null 时,则会从 appContext 上下文中获取 provides 对象,否则将从当前实例的父组件实例中获取 provides 对象。

    对于我们的示例来说,在获取到 provides 对象后,就会判断 name 属性是否存在于当前的 provides 对象中,此时该对象是 { name: "阿宝哥"},所以会直接返回 "阿宝哥"。

    此外,通过观察 inject 函数,我们还可以得出以下结论:

    四、App 对象中的 provide API

    在创建完 Vue 3 应用对象之后,我们可以使用该对象提供的 provide 方法。该方法设置一个可以被注入到应用范围内所有组件中的值。之后,组件就可以使用 inject 来接收 provide 的值。

     
     
     
     
    1. import { createApp } from 'vue' 
    2.  
    3. const app = createApp({ 
    4.   inject: ['name'], 
    5.   template: ` 
    6.     
       
    7.       {{ name }} 
    8.     
     
  •   ` 
  • }) 
  •  
  • app.provide('name', '阿宝哥') 
  • 需要注意的是,app.provide 方法不应该与 provide 组件选项或组合式 API 中的 provide 方法混淆。虽然它们也是相同的 provide/inject 机制的一部分,但是是用来配置组件 provide 的值而不是应用 provide 的值。

    介绍完 app.provide 方法之后,我们来了解一下它的实现。看过 ”Vue 3.0 进阶“ 系列教程的小伙伴,对 app 对象应该不会陌生。因为在前面的文章中,阿宝哥已经介绍过 component、directive 和 mount 等方法。接下来,我们来看一下 provide 方法的具体实现:

     
     
     
     
    1. // packages/runtime-core/src/apiCreateApp.ts 
    2. export function createAppAPI
    3.   render: RootRenderFunction, 
    4.   hydrate?: RootHydrateFunction 
    5. ): CreateAppFunction { 
    6.   return function createApp(rootComponent, rootProps = null) { 
    7.     const context = createAppContext() 
    8.  
    9.     // 省略大部分内容 
    10.     const app: App = (context.app = { 
    11.       _uid: uid++, 
    12.       _context: context, 
    13.  
    14.       provide(key, value) { 
    15.         // TypeScript doesn't allow symbols as index type 
    16.         // https://github.com/Microsoft/TypeScript/issues/24587 
    17.         context.provides[key as string] = value 
    18.         return app 
    19.       } 
    20.     }) 
    21.  
    22.     return app 
    23.   } 

    由以上代码可知,在 provide 方法内部会把 key 和 value 以键值对的形式保存在应用上下文 context 对象的 provides 属性中。

     
     
     
     
    1. // packages/runtime-core/src/apiCreateApp.ts 
    2. export function createAppContext(): AppContext { 
    3.   return { 
    4.     app: null as any, 
    5.     config: { ... }, 
    6.     mixins: [], 
    7.     components: {}, 
    8.     directives: {}, 
    9.     provides: Object.create(null) 
    10.   } 

    五、阿宝哥有话说

    5.1 在嵌套的 providers 场景下,存在同名的 key 会怎么样?

     
     
     
     
     
  •  
  • 在以上代码中,ProviderOne 和 ProviderTwo 组件中使用同样的 foo 属性名配置了 Provider。然后,我们在底层的 Consumer 组件中使用 inject API 分别注入了 ProviderOne 和 ProviderTwo 中配置的值。接下来,我们先来看一下结果:

     
     
     
     
    1. fooOverride,bar,baz 

    由以上结果可知,在嵌套 providers 的场景中,会就近从父组件实例中获取对应的值,找不到的话,会往上一层层进行查找。

    5.2 通过 inject 获取响应式的值,能否正常工作?

    在某些场景下,我们希望往深层的子组件传递通过响应式 API 创建的响应式的值,那么通过 inject 函数获取的响应式的值可以正常工作么?要弄清楚这个问题,我们来看一个具体的示例:

     
     
     
     
    1.  
    2.  

    在以上代码中,我们通过 ref API 创建了一个 nameRef 对象,然后在根组件中通过 provide 函数配置相应的 Provider。而在 Consumer 组件的 setup 方法内,我们通过 inject 函数注入了 nameRef 对象,并通过 name.value 访问了该对象内保存的值。

    此外,在 setup 方法内部,我们还使用了 onMounted 生命周期钩子,在钩子对应的回调函数中,我们延迟 2S 修改 nameRef 对象的值。

    以上示例成功运行后,首先会先显示 大家好,我是阿宝哥!,差不多 2S 后页面会刷新为 大家好,我是kakuqo!。

    5.3 是否支持 self-inject?

    什么是 self-inject 呢?这里阿宝哥不做过多解释,我们直接来看个具体的例子:

     
     
     
     
    1.  
    2.  

    在以上代码中,我们在 Provider 组件的 setup 方法内部先使用 provide 函数配置了相应的 Provider,然后使用 inject 函数来获取对应的值。很明显这个操作并没有实际的意义,那么可以这样使用么?答案是可以的,以上示例成功运行之后,Provider 组件会被转换为 注释节点。

     
     
     
     
    1.  

    那么为什么会转为注释节点呢?因为 injectedName 的值为 undefined,在通过 h 函数创建 VNode 对象的时候,会继续调用 createVNode 函数,在该函数内部如果发现是 type 类型为 falsy 值时,会把 VNode 对象的类型统一转换为 Comment 类型。

     
     
     
     
    1. // packages/runtime-core/src/vnode.ts 
    2. function _createVNode( 
    3.   type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, 
    4.   props: (Data & VNodeProps) | null = null, 
    5.   children: unknown = null, 
    6.   patchFlag: number = 0, 
    7.   dynamicProps: string[] | null = null, 
    8.   isBlockNode = false 
    9. ): VNode { 
    10.   if (!type || type === NULL_DYNAMIC_COMPONENT) { 
    11.     if (__DEV__ && !type) { 
    12.       warn(`Invalid vnode type when creating vnode: ${type}.`) 
    13.     } 
    14.     type = Comment 
    15.   } 
    16.   // 省略大部分代码 

    本文阿宝哥主要介绍了依赖注入的概念及作用、如何使用 Vue 3 提供的 provide/inject 特性。为了让大家能够更深入地理解 provide/inject 特性,阿宝哥从源码角度分析了 provide 和 inject 函数的具体实现。在后续的文章中,阿宝哥将会介绍在插件中如何应用 provide/inject 特性,感兴趣的小伙伴不要错过哟。

    六、参考资源

    Vue 3 官网 - Provide/Inject

    Vue 3 官网 - 组合式 API

    标题名称:Vue3.0进阶之依赖注入探秘
    本文链接:http://www.gawzjz.com/qtweb2/news6/20006.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    App开发知识

    同城分类信息