Provide/Inject

Provide 和 Inject 可以解决 Prop 逐级透传问题。注入值类型不会使注入保持响应性,但注入一个响应式对象,仍然有响应式的效果。

Provide 的问题是无法追踪数据的来源,在任意层级都能访问导致数据追踪比较困难,不知道是哪一个层级声明了这个或者不知道哪一层级或若干个层级使用了。

看看 VueUse 的 createInjectionState 是怎么封装 Provide 的,并且怎么避免 Provide 的问题。

介绍

createInjectionState:创建可以注入组件的全局状态。

//useCounterStore.ts
const [useProvideCounterStore, useCounterStore] = createInjectionState(
  (initialValue: number) => {
    // state
    const count = ref(initialValue)

    // getters
    const double = computed(() => count.value * 2)

    // actions
    function increment() {
      count.value++
    }

    return { count, double, increment }
  })

export { useProvideCounterStore }
// If you want to hide `useCounterStore` and wrap it in default value logic or throw error logic, please don't export `useCounterStore`
export { useCounterStore }
<!-- RootComponent.vue -->
<script setup lang="ts">
  import { useProvideCounterStore } from './useCounterStore'

  useProvideCounterStore(0)
</script>

<template>
  <div>
    <slot />
  </div>
</template>
<!-- CountComponent.vue -->
<script setup lang="ts">
import { useCounterStore } from './useCounterStore'

// use non-null assertion operator to ignore the case that store is not provided.
const { count, double } = useCounterStore()!
// if you want to allow component to working without providing store, you can use follow code instead:
// const { count, double } = useCounterStore() ?? { count: ref(0), double: ref(0) }
// also, you can use another hook to provide default value
// const { count, double } = useCounterStoreWithDefaultValue()
// or throw error
// const { count, double } = useCounterStoreOrThrow()
</script>

<template>
  <ul>
    <li>
      count: {{ count }}
    </li>
    <li>
      double: {{ double }}
    </li>
  </ul>
</template>

源码

/**
 * Create global state that can be injected into components.
 *
 * @see https://vueuse.org/createInjectionState
 *
 */
export function createInjectionState<Arguments extends Array<any>, Return>(
  composable: (...args: Arguments) => Return,
): readonly [useProvidingState: (...args: Arguments) => Return, useInjectedState: () => Return | undefined] {
  const key: string | InjectionKey<Return> = Symbol('InjectionState')
  const useProvidingState = (...args: Arguments) => {
    const state = composable(...args)
    provide(key, state)
    return state
  }
  const useInjectedState = () => inject(key)
  return [useProvidingState, useInjectedState]
}

思考

为什么返回的是数组

createInjectionState 返回的数组,使用 demo 中采用的数组解构的方式。那么数组解构和对象解构有什么区别么?

提到数组解构首先想到的是 react 的 useState。

const [count,setCount] =useState(0)

之所以用数组解构是因为在调用多个 useState 的时候,方便命名变量。

const [count,setCount] =useState(0)
const [double, setDouble] = useState(0);

如果用对象解构,代码会是

const {state:count,setState:setCount} =useState(0)
const {state:double, setState:setDouble} = useState(0);

相比之下数组显得代码更加简洁。

数组解构也有缺点:返回值必须按顺序取值。返回值中只取其中一个,代码就很奇怪。

const [,setCount] =useState(0)

因此数组解构时适合使用所有返回值,并且多次调用方法的情况;对象解构适合只使用其中部分返回值,并且一次调用方法的情况。

createInjectionState 创建的注入状态 key 是 Symbol('InjectionState'),也就是每次运行的 key 都不一样,有可能多次调用 createInjectionState,因此 createInjectionState 采用数组解构的方式。但使用返回值可能只使用 useInjectedState,所有在 useCounterStore.ts 中又将 useProvideCounterStore 和 useInjectedState 以对象的方式导出避免出现下面奇怪的写法。

const [,useCounterStore] =useCounterStore()

使用例子中的 state 结构

使用案例中将 provide 中的对象分为 state、getters、actions。结构很想 vuex,而 useProvideCounterStore 相当于 vuex 中的 mutation。采用这种结构是因为 provide 的缺点:无法追踪数据的来源,在任意层级都能访问导致数据追踪比较困难,不知道是哪一个层级声明了这个或者不知道哪一层级或若干个层级使用了。

采用类似 vuex 的结构能相对比较好的追踪状态。

 // state
  const count = ref(initialValue)

  // getters
  const double = computed(() => count.value * 2)

  // actions
  function increment() {
    count.value++
  }

readonly

createInjectionState 返回的数组是 readonly 修饰的,useInjectedState 返回的对象并没有用 readonly 修饰,provide/inject 的缺点就是状态对象不好跟踪,容易导致状态变更失控。既然提供了 useProvidingState 修改状态的方法,useInjectedState 返回的状态如果是只读的能更好防止状态变更失控。

05-08 00:42