当前位置: 首页 > 科技观察

Vue3.0高级依赖注入探索

时间:2023-03-13 14:04:22 科技观察

本文转载自微信公众号《全栈修真之路》,作者阿宝哥。转载本文请联系全栈修真之路公众号。本文是Vue3.0进阶系列的第六篇文章。在这篇文章中,阿宝哥将带大家探索一下Vue3中的依赖注入功能,用过Angular的朋友应该对依赖注入不陌生,简称DI(DependencyInjection)。组件之间的依赖关系由容器在运行时决定。说白了就是容器动态的给组件注入一定的依赖。依赖注入的目的不是给软件系统带来更多的功能,而是增加组件重用的频率,为系统搭建一个灵活、可扩展的平台。在Vue3.0中,为我们提供了一个简单的依赖注入功能——provide/inject。它们解决了一些组件嵌套较深的问题,较深的子组件只需要父组件的部分内容。在这种情况下,如果props仍然沿组件链向下传递,使用起来会很麻烦。为了解决以上问题,Vue提供了provide和inject。使用provide/inject后,无论组件层级有多深,父组件都可以充当其所有子组件的依赖提供者。(图片来源-https://v3.cn.vuejs.org/guide/component-provide-inject.html)从上图可以看出,父组件通过provide提供数据,子组件通过provide注入数据注入。介绍完provide/inject的功能,我们来看一个具体的例子。1.Provide/Inject用法示例在使用provide/inject功能时,您可以通过provide和inject选项来使用它。该方法的官方文档已经介绍过了。这里阿宝哥再介绍一种使用方法,就是在组合API的setup组件选项中,通过provide和inject函数实现依赖注入。

在上面的例子中,数据是通过Provider组件中的provide函数配置的,并且在Consumer组件中,通过inject函数获取Provider组件中配置的数据。需要注意的是,示例中的Consumer组件是Provider组件的孙子。因此,通过使用provide/inject提供的依赖注入功能,我们实现了数据的跨层传递。介绍完provide和inject函数的基本使用,阿宝哥就带大家一起揭开它们背后的秘密。2.provide函数在分析provide函数之前,先回顾一下它的用法:constProvider={setup(){provide('name','Abaoge')return()=>h(Middle)}}该函数定义在runtime-core/src/apiInject.ts文件://packages/runtime-core/src/apiInject.tsexportinterfaceInjectionKeyextendsSymbol{}exportfunctionprovide(key:InjectionKey|string|number,value:T){if(!currentInstance){if(__DEV__){warn(`provide()canonlybeusedinsidesetup().`)}}else{letprovides=currentInstance.provides//默认情况下,组件实例会继承它的provides对象父组件实例,当它需要为自己提供//值时,它会使用父组件的provides对象作为原型对象来创建自己的provides//对象。这样,`inject`函数可以简单地从直接父对象中查找要注入的值,然后让原型链//完成工作。constparentProvides=currentInstance.parent&¤tInstance.parent.providesif(parentProvides===provides){provides=currentInstance.provides=Object.create(parentProvides)}//TS不允许使用symbol作为索引类型provides[keyasstring]=value}}通过观察上面的代码,我们可以得出如下结论:provide函数只能用在组合API的setup函数中。组件实例上会有一个provides属性,通过provide配置的数据最终会保存在组件的provides属性中。provide函数支持3种类型作为key,即InjectionKey|字符串|数字,其中InjectionKey类型是Symbol类型的子类型。在上面的代码中,我们看到了currentInstance对象,那么这个对象的内部结构是怎样的呢?为了一探究竟,我们在provide函数内部加了一个断点:从上图可以看出,currentInstance是一个普通对象。其中bc(BEFORE_CREATE)、bm(BEFORE_MOUNT)和bu(BEFORE_UPDATE)是生命周期相关的钩子。那么问题又来了,currentInstance对象是如何创建的呢?看过之前Vue3.0进阶系列文章的朋友,可能对createComponentInstance这个函数还有一点印象。该函数的作用是创建一个组件实例。具体代码如下://packages/runtime-core/src/component.tsexportfunctioncreateComponentInstance(vnode:VNode,parent:ComponentInternalInstance|null,suspense:SuspenseBoundary|null){consttype=vnode.typeasConcreteComponent//inheritparentappcontext-or-ifroot,adoptfromrootvnodeconstappContext=(parent?parent.appContext:vnode.appContext)||emptyAppContextconstinstance:ComponentInternalInstance={uid:uid++,vnode,type,parent,appContext,root:null!,//立即设置子树:null!,//将在创建更新后同步设置更新:null!,//willbesetsynchronouslyrightaftercreationeffects:null,:null,provides:parent?parent.provides:Object.create(appContext.provides),bc,bm,bu//省略大部分属性}//省略部分代码instance.root=parent?parent.root:instanceinstance.emit=emit.bind(null,instance)returninstance}需要注意的是,如果当前组件的parent属性的值不为null,则parent.provides的值会作为属性提供财产的组件实例介绍完provide和createComponentInstance函数后,为了让大家更好的理解前面的例子,宝哥用一张图来总结一下例子中组件之间的关系:对于根组件来说,它的parent属性值为null。好了,provide函数就先介绍到这里,下面开始介绍inject函数。3.注入函数同样,在分析注入函数之前,我们先回顾一下它的用法:constConsumer={setup(){constname=inject('name')return()=>`大家好,这里是${名字}!`}}inject函数和provide函数相互配合,都定义在runtime-core/src/apiInject.ts文件中://packages/runtime-core/src/apiInject.tsexportfunctioninject(key:InjectionKey|string,defaultValue?:unknown,treatDefaultAsFactory=false){constinstance=currentInstance||currentRenderingInstance//获取当前实例if(instance){//tosupport`app.use`plugins,//fallbacktoappContext's`provides`iftheintanceisatrootconstprovides=instance.parent==null?instance.vnode.appContext&&instance.vnode.appContext.provides:instance.parent.providesif(provides&&(keyasstring|symbol)inprovides){//TS不允许symbolasindextypereturnprovides[keyasstring]}elseif(arguments.length>1){returntreatDefaultAsFactory&&isFunction(defaultValue)?defaultValue():defaultValue}elseif(__DEV__){warn(`injection"${String(key)}"notfound.`)}}elseif(__DEV__){warn(`inject()canonlybeusedinsidesetup()orfunctionalcomponents.`)}}在inject函数中,我们可以清楚的看到,如果当前实例的parent属性为null,则provides对象将从appContext上下文中获取,否则为当前实例的父组件实例instance会获取获取provides对象在我们的例子中,获取到provides对象后,会判断当前provides对象中是否存在name属性。这时对象是{name:"阿宝哥"},所以会直接返回给"阿宝哥"。另外,通过观察inject函数,我们还可以得出以下结论:inject函数的第二个参数是一个可选参数——defaultValue?:unknown,用于设置默认值或默认值工厂。即当在provides对象上找不到key对应的值时,可以使用默认值或者默认值工厂的返回值代替。inject函数只能在setup()或函数组件中使用。4、App对象中的provideAPI创建好Vue3应用对象后,我们就可以使用该对象提供的provide方法了。该方法设置了一个值,可以注入到应用程序范围内的所有组件中。之后,组件可以使用inject来接收provide的值。import{createApp}from'vue'constapp=createApp({inject:['name'],template:`
{{name}}}
`})app.provide('name','阿宝哥')请注意,app.provide方法不应与provide组件选项或组合API中的provide方法混淆。尽管它们也是相同的提供/注入机制的一部分,但用于配置组件提供的值而不是应用程序提供的值。介绍完app.provide方法,我们来看看它的实现。看过《Vue3.0进阶》系列教程的朋友应该对app对象不陌生。因为在之前的文章中,阿宝哥已经介绍过component、directive、mount等方法。接下来我们看一下provide方法的具体实现://packages/runtime-core/src/apiCreateApp.tsexportfunctioncreateAppAPI(render:RootRenderFunction,hydrate?:RootHydrateFunction):CreateAppFunction{returnfunctioncreateApp(rootComponent),rootProps=null){constcontext=createAppContext()//省略大部分内容/github.com/Microsoft/TypeScript/issues/24587context.provides[keyasstring]=valuereturnapp}})returnapp}}从上面的代码中,我们可以看到键和值将以键值对的形式存储在对象的provides属性中的provide方法内部。//packages/runtime-core/src/apiCreateApp.tsexportfunctioncreateAppContext():AppContext{return{app:nullasany,config:{...},mixins:[],components:{},directives:{},provides:Object.create(null)}}5.阿宝哥有话要说5.1nestedproviders场景下如果有同名key怎么办?
上面代码中Provider配置了和ProviderOne中相同的foo属性名和ProviderTwo组件。然后,我们使用底层Consumer组件中的injectAPI分别注入ProviderOne和ProviderTwo中配置的值。接下来我们看一下结果:fooOverride,bar,baz从上面的结果可以看出,在嵌套provider的场景下,会从就近的父组件实例中获取对应的值。如果找不到,它会向上一层搜索。5.2通过inject获取的responsive值是否可以正常工作?在某些场景下,我们希望将通过reactiveAPI创建的reactivevalue传递给深层子组件,那么通过inject函数获取的reactivevalue是否可以起作用呢?为了说明这个问题,让我们看一个具体的例子:constapp=createApp({setup(){provide("name",nameRef);return()=>h(Middle)}})constMiddle={render:()=>h(Consumer)}constConsumer={setup(){constname=inject('name')onMounted(()=>{setTimeout(()=>nameRef.value="kakuqo",2000);})return()=>`大家好,我是${name。value}!`}}app.mount('#app')在上面的代码中,我们通过refAPI创建了一个nameRef对象,然后通过根组件中的provide函数配置对应的Provider。在Consumer组件的setup方法中,我们通过inject函数注入了nameRef对象,并通过name.value访问对象中保存的值。此外,在setup方法内部,我们还使用了onMounted生命周期钩子。在hook对应的回调函数中,我们延迟2S修改nameRef对象的值。上面的例子运行成功后首先会显示大家好,我是阿宝哥!大约2秒后,页面将刷新为大家好,我是kakuqo!。5.3是否支持自注入?什么是自注?阿宝哥这里就不多解释了,看一个具体的例子:
在上面的代码中,我们首先使用provide函数在Provider组件的setup方法中配置对应的Provider,然后使用inject函数获取对应的值。显然这个操作没有什么实际意义,难道可以这么用吗?答案是肯定的,上面的例子运行成功后,Provider组件会被转换为评论节点。
那为什么会变成评论节点呢?因为injectedName的值是undefined,所以当通过h函数创建VNode对象时,会继续调用createVNode函数。如果在函数内部发现类型type为falsy值,则将VNode对象的类型统一转换为Comment类型。//packages/runtime-core/src/vnode.tsfunction_createVNode(type:VNodeTypes|ClassComponent|typeofNULL_DYNAMIC_COMPONENT,props:(Data&VNodeProps)|null=null,children:unknown=null,patchFlag:number=0,dynamicProps:string[]|null=null,isBlockNode=false):VNode{if(!type||type===NULL_DYNAMIC_COMPONENT){if(__DEV__&&!type){warn(`Invalidvnodetypewhencreatingvnode:${type}.`)}type=Comment}//省略大部分代码}本文主要介绍依赖注入的概念和作用,以及如何使用Vue3提供的provide/inject特性,为了让大家对provide/inject特性有更深入的了解,阿宝哥从源码的角度分析provide和inject函数的具体实现。在后续的文章中,阿宝哥会介绍如何在插件中应用provide/inject特性,感兴趣的朋友不要错过。6.参考资源Vue3官网-提供/InjectVue3官网-组合API