提到依赖注入 - Provide/Inject,我们的第一反应就是组件间通信, 相比 props 来说会更加灵活自由,看下面的图就能明白了。
DeepChild 组件为了消费 Root 组件中的数据,我们需要在整个组件树中层层传递该数据,随着组件树层级结构复杂度的提升,对于 props 的处理愈发繁琐。幸运的是,Vue 为我们提供的 Provide/Inject 方案完美解决了这一痛点。
关于 Provide/Inject 的基本使用就不再照本宣科了。这里提几个使用中要注意的点。
区分组件级别和应用级别的 Provide/Inject
组件级别就是 A 组件 Provide,B 组件 Inject;而应用级别是在根组件 Provide,所有子组件都可以 Inject。
// 组件级别
<script setup>
import { provide } from 'vue'
provide(/* key */ 'message', /* value */ 'hello!')
</script>
// 应用级别
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* key */ 'message', /* value */ 'hello!')
可作用于响应式数据
Provide 可以提供响应式数据,这样 Inject 的组件可以与 Provide 的组件建立响应式联系。
避免在 Inject 组件中修改源数据
Inject 组件应该只消费源数据,对于源数据的变更应该交给 Provide 组件,这样方便后期维护。为了避免 Inject 组件修改源数据,我们可以在 Provide 组件中将其声明为只读 readonly。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
如果确实有需要在 Inject 组件中修改源数据的需求,可以这样。
// Provide 组件
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
// Inject 组件
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
在 Provide 组件中为源数据配套提供一个修改方法,在 Inject 组件中调用该方法来修改源数据。有点类似 emit 的味道。
尽量使用 Symbol 做为 InjectionKey
如果使用常规的字符串做为 InjectionKey,很可能会与项目中的其它 InjectionKey 发生冲突,特别是对于多人协作的大型项目。而通过 Symbol() 构造的 key 始终都是唯一的。
上面说到的这些针对的大都是一般的应用开发,在一些开源库中对于 Provide/Inject 的使用则现得更为巧妙。
下面我们以 Vue Router4.x 为例。
先看Vue Router 的使用。
// ./router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [];
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router).mount('#app')
main.js 中通过 app.use 安装路由插件 router,这里的 router 是通过 Vue Router 中暴露的 ceateRouter API 创建的。
const routerKey = Symbol('router')
const routeLocationKey = Symbol('route location' : '')
export function createRouter(routerOptions) {
//
return {
// 路由方法
push,
replace,
go,
// 路由守卫
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
install(app) {
// 注册全局路由组件
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// router 挂载到全局变量 $router
app.config.globalProperties.$router = router
// 提供 router reactiveRouter currentRoute
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
}
}
}
这里,我们关注通过 app.provide API 提供的 router(路由实例) 和 reactiveRoute(当前路由)。
在 Vue3 setup 函数中,我们是这样访问路由实例和当前路由的。
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
router.push({
name: 'search',
query: {
...route.query,
},
})
}
},
}
注意 Vue Router 暴露的两个 API - useRouter、useRoute
export function useRouter(): Router {
return inject(routerKey)!
}
export function useRoute(): RouteLocationNormalizedLoaded {
return inject(routeLocationKey)!
}
实现很简单,就是通过 inject 注入了在前面通过 app.provide 提供的 router 和 route 对象。很明显这种属于前面提到的应用级别的依赖注入。
对于 Provide/Inject 的这种使用方式在开源库中十分常见,比如,Vue 生态中的状态库 Pinia,感兴趣的可以自己去翻源码。