深入 Vue 依赖注入-Provide/Inject

610 阅读3分钟

提到依赖注入 - Provide/Inject,我们的第一反应就是组件间通信, 相比 props 来说会更加灵活自由,看下面的图就能明白了。

prop-drilling.11201220.png

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 - useRouteruseRoute

export function useRouter(): Router {
  return inject(routerKey)!
}

export function useRoute(): RouteLocationNormalizedLoaded {
  return inject(routeLocationKey)!
}

实现很简单,就是通过 inject 注入了在前面通过 app.provide 提供的 router 和 route 对象。很明显这种属于前面提到的应用级别的依赖注入。

对于 Provide/Inject 的这种使用方式在开源库中十分常见,比如,Vue 生态中的状态库 Pinia,感兴趣的可以自己去翻源码。