VueUse 是怎麼封裝Vue3 Provide/Inject 的?

千空_石紀發表於2023-05-07

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 返回的狀態如果是隻讀的能更好防止狀態變更失控。

相關文章