Vue3.0——Provide/Inject的妙用 说起Vue的状态管理工具,应该大部分人都会想到Vuex,毕竟是官方提供的工具,但是都进入Vue3时代了,不会还有人不知道依赖注入吧。 为什么会突然提到依赖注入呢?跟状态管理工具有什么关系呢? 当然有关系,因为依赖注入在Vue3中可以替代Vuex。我们知道Vue3提供的ref/reactive API具有组件解耦的特性,也就是说我们可以在组件之外创建响应式数据,这么一来跨组件共享数据的需求就在Vue3新框架内部得到了解决。
Provide / Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。如果组件层级比较深,那通过props传递下去会比较麻烦。针对这种情况我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
Provide / Inject不是什么新的api,在Vue2的时候就已经存在了,只不过在Vue3得到新的应用。这里只做一个简单的介绍,需要详细的介绍见官方文档 。
我们理解一些官方给出的示例图,粗暴点来说就是父组件注入一些数据,所有子组件不管嵌套有多深都能直接获取到这些数据。
Provide / Inject 使用方式 首先在父级组件App.vue
注入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <button @click="editUserInfo" >修改用户信息</button> </template> <script lang="ts" > import { defineComponent, provide, reactive } from 'vue' import Child from './components/Child.vue' export default defineComponent({ name: 'App' , components: { Child }, setup() { const userInfo = reactive({ username: '' , age: 25 }) provide('userInfo' , userInfo) const editUserInfo = () => { userInfo.age = 18 userInfo.username = 'Tab' } return { userInfo, editUserInfo } } }) </script>
子组件获取注入的数据项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class ="inject-box" > username: {{ userInfo.username }}, age : {{ userInfo.age }} </div> </template> <script lang="ts" > import { reactive, defineComponent, inject } from 'vue' export default defineComponent({ name: 'Child' , setup: () => { const userInfo = inject('userInfo' ); return { userInfo } } }) </script>
Provide / Inject 原理 provide/inject原理并不复杂,源码的实现也很简单。在看源代码之前,我们可以先看一眼Vue
实例上有没有provide
和inject
的线索。如果Vue
实例上就存在相关属性,那么我们就需要从Vue
实例的声明开始看源码,否则就可以猜测provide
和inject
是工具函数,直接搜对应的代码实现即可。
万能的console.log
查看Vue
实例属性。(Vue3
不能直接打印this
,需要导入getCurrentInstance
来获取当前实例)
1 2 3 4 5 6 7 8 9 10 11 <script lang="ts" > import { defineComponent, getCurrentInstance } from 'vue' export default defineComponent({ name: 'HelloWorld' , setup: () => { const instance = getCurrentInstance() console .log(instance) } }) </script>
查看控制台日志发现,Vue
实例中存在一个provides
属性,那就说明Vue
实例声明的时候肯定做了一些什么才会有了provide
和inject
的存在。
然后我们就可以转到源码声明Vue
实例的地方,源码位置./packages/runtime-core/src/component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export function createComponentInstance ( vnode: VNode, parent: ComponentInternalInstance | null , suspense: SuspenseBoundary | null ) { const type = vnode.type as ConcreteComponent const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext const instance: ComponentInternalInstance = { uid: uid++, vnode, type , parent, appContext, provides: parent ? parent.provides : Object .create(appContext.provides), } if (__DEV__) { instance.ctx = createRenderContext(instance) } else { instance.ctx = { _: instance } } instance.root = parent ? parent.root : instance instance.emit = emit.bind(null , instance) return instance } export function createAppContext ( ): AppContext { return { provides: Object .create(null ) } }
上述代码可以看出,每个组件的provides
都是继承自父组件的provides
,根组件的provides
其实是个空对象 。
看到这里大致可以猜出来provide/inject
是什么了,剩下我们只要了解下provide
和inject
两个工具函数的原理就能明白其中的缘由了。
provide
方法原理
provide 方法的作用是往当前实例provides写入新的key-value,如果key已存在则覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export function provide <T >(key: InjectionKey<T> | string | number , value: T ) { if (!currentInstance) { if (__DEV__) { warn(`provide() can only be used inside setup().` ) } } else { let provides = currentInstance.provides const parentProvides = currentInstance.parent && currentInstance.parent.provides if (parentProvides === provides) { provides = currentInstance.provides = Object .create(parentProvides) } provides[key as string ] = value } }
inject
方法原理
inject 方法的作用是从当前实例的provides获取key对应的value值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export function inject ( key: InjectionKey<any > | string , defaultValue?: unknown, treatDefaultAsFactory = false ) { const instance = currentInstance || currentRenderingInstance if (instance) { const provides = instance.parent == null ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides if (provides && (key as string | symbol) in provides) { return provides[key as string ] } else if (arguments .length > 1 ) { return treatDefaultAsFactory && isFunction(defaultValue) ? defaultValue() : defaultValue } else if (__DEV__) { warn(`injection "${String (key)} " not found.` ) } } else if (__DEV__) { warn(`inject() can only be used inside setup() or functional components.` ) } }
源码总结:子组件继承父组件的provides
属性,从而达到深层的组件也能访问到注入组件的数据。
简易版Provide / Inject 通过上面源码的学习,我们可以自己写一个简易版的Provide / Inject
,原理就是es6的class继承,直接附上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Root { static provides = {} constructor () {} provide(key, value) { Root.provides[key] = value } inject(key, defaultValue, treatDefaultAsFactory) { if (key && key in Root.provides) { return Root.provides[key] } else if (arguments .length > 1 ) { return treatDefaultAsFactory && typeof defaultValue === 'function' ? defaultValue() : defaultValue } } } class Parent extends Root { constructor () { super (); } provide(key, value) { super .provide(key, value) } } class Child extends Parent { constructor () { super (); } } let rootInstance = new Root();rootInstance.provide('username' , 'Yxx' ) let parentInstance = new Parent();let instance = new Child();parentInstance.provide('age' , 18 ); console .log(instance.inject('age' ))console .log(instance.inject('username' ))
应用场景(妙用) 重点来了,了解完原理之后我们来看下Provide / Inject
的应用场景,如何来代替现在的状态管理工具Vuex
呢?
首先创建一个store
文件夹(文件夹名称可随意修改)用于存放共享数据相关的代码,并且创建一个入口文件index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { reactive, readonly } from "vue" ;export interface Store { state: { count: number ; }, increment(): void , } const state = reactive({ count: 0 , }); const increment = (): void => { state.count++; } export const global : Store = { state: readonly (state), increment, }
然后在根组件(App.vue
)注入依赖数据
1 2 3 4 5 6 7 8 9 10 11 12 13 <script lang="ts" > import { defineComponent, getCurrentInstance } from 'vue' import { global } from './store/index' export default defineComponent({ name: 'App' , provide: { global }, }) </script>
最后子组件通过inject
获取注入的数据,以及调用相关的方法来修改注入的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class ="inject-box" > <p>{{ global .state.count }}</p> <button @click ="global.increment" >++</button> </div> </template> <script lang="ts" > import { ref, defineComponent} from 'vue' export default defineComponent({ name: 'HelloWorld' , inject: ['global' ], setup: () => {} }) </script>
通过前3步,已经完成了全局状态管理。但是有一个问题,当项目特别庞大,全部的状态数据存在一份代码里面肯定是不合理的。所以接下去再实现下模板化,使状态数据能模块化管理。
创建modules
文件夹存放模块数据,并创建user.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { reactive, readonly } from "vue" ;export interface UserStore { state: { username: string ; }, editUserName(username: string ): void } const state = reactive({ username: "" }) const editUserName = (username: string ) => { state.username = username; } export const User = reactive({ state: readonly (state), editUserName });
在store/index.ts
导入user
模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { reactive, readonly , toRefs } from "vue" ;import { User } from './modules/user' ;const state = reactive({ count: 0 , ...toRefs(User.state) }); export const global : Store = { state: readonly (state), editUserName: User.editUserName }
最后我们在子组件调用即可,方法同第3步类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <template> <div class ="inject-box" > <p>{{ global .state.count }}</p> <button @click ="global.increment" >++</button> </div> <div class ="inject-box" > <p>{{ global .state.username }}</p> <input type ="text" v-model="uname" /> </div> </template> <script lang="ts" > import { ref, defineComponent, inject, watch } from 'vue' import { Store } from '../store/index' export default defineComponent({ name: 'HelloWorld' , inject: ['global' ], setup: () => { const uname = ref('' ) const global : Store | undefined = inject('global' ) watch(uname, (newValue: string ): void => { global && global .editUserName(newValue) }) return { uname } } }) </script>