順藤摸瓜:用單元測試讀懂 vue3 中的 provide/inject

React Context API 提供了一種 Provider 模式,用以在組件樹中的多個任意位置的組件之間共享屬性,從而避免必須在多層嵌套的結構中層層傳遞 props。其圍繞 Context 的概念,分別提供了 Provider 和 Comsumer 兩種對象。前端

雖然 API 不一樣,且更傾向用於插件,但 Vue 中一樣提供了 Provider 模式。好比 Vue 2.x 文檔中對此的描述是:vue

這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。...... provide 和 inject 主要在開發高階插件/組件庫時使用。react

Vue 3.x 的組合式 API 中也提供了兩個相似的獨立函數,Composition API RFC 中寫道:git

許多 Vue 的插件都向 this 注入 property ...... 當使用組合式 API 時,咱們再也不使用 this,取而代之的是,插件將在內部利用 provide 和 inject 並暴露一個組合函數。github

延續系列的主題,本文將繼續嘗試立足於相關模塊的單元測試解讀和適度源碼分析,主要考察 Vue 3.x Composition API 中的 provide() 和 inject() 兩個方法;但願能在結合閱讀文檔的基礎上,更好地理解相關模塊。web

咱們將要觀察三個代碼倉庫,分別是:api

  • vue - Vue 2.x 項目
  • @vue/composition-api - 結合 Vue 2.x 「提早嚐鮮」 Composition API 的過渡性項目
  • vue-next - Vue 3.x 項目,本文分析的是其 3.0.0-beta.15 版本

🔀 Vue 2.x + @vue/composition-api

1.1 函數簽名

// composition-api/src/apis/inject.ts 

export function provide<T>(
    key: InjectionKey<T> | string
    value: T
): void

export function inject<T>(
    key: InjectionKey<T> | string
): T | undefined

export function inject<T>(
    key: InjectionKey<T> | string
    defaultValue: T
): T

interface InjectionKey<Textends Symbol 
{}

1.2 測試用例

考察 composition-api/test/apis/inject.spec.js 文件:微信

test 1: 'Hooks provide/inject - should work'

  • 在根組件的 setup() 中,調用兩次 provide(),並分別指定 Ref 和 Boolean 類型的 values
  • 根組件加載後,在消費者子組件中,能正確獲得以上 values

test 2: 'should return a default value when inject not found'

  • 在組件的 setup() 中,調用 inject(不存在的key, defaultValue)
  • 組件加載後,上述 inject() 返回值爲傳入的 defaultValue

test 3: 'should work for ref value'

  • const Msg = Symbol() 做爲 key
  • 在根組件的 setup() 中,provide() 中傳入 Ref 類型的 value1
  • 在子組件的 setup() 的 return 中,返回 msg: inject(Msg)
  • 根組件加載後,當即以 app.$children[0].msg = 'bar' 的形式賦新值
  • 在 nextTick 中,應渲染出新傳入的值 'bar'

test 4: 'should work for reactive value'

  • const State = Symbol() 做爲 key
  • 在根組件的 setup() 中,調用 provide( State, reactive({ msg: 'foo' }) )
  • 在子組件的 setup() 的 return 中,返回 state: inject(State)
  • 根組件加載後,當即以 app.$children[0].state.msg = 'bar' 的形式賦新值
  • 在 nextTick 中,應渲染出新傳入的值 'bar'

test 5: 'should work when combined with 2.x provide option'

  • 在根組件中,分別在 setup() 中調用 provide() 以及在 provide Options API 中指定屬性
  • 在子組件的 setup() 中,能正確 inject() 到以上兩種賦值

1.3 調用關係

簡單分析源碼,主要函數的調用關係爲:app

1.4 部分概括

  • 核心部分還是 Vue 2.x 中已經實現的 vm._provided 內部對象
    • 和原有的 Options API 中的 provide/inject 屬性達到統一處理的效果
    • inject() 只能在 setup() 或 functional component 中使用
  • 在用例 test 三、test 4 中,順帶能夠看出,直接從 vue 實例上訪問 Ref 值是不用 .value 的;其基本實現以下:
// src/setup.ts 

function asVmProperty(
  vm: ComponentInstance,
  propName: string,
  propValue: Ref<unknown>
{
  const props = vm.$options.props
  if (!(propName in vm) && !(props && hasOwn(props, propName))) {
    proxy(vm, propName, {
      get() => propValue.value,
      set(val: unknown) => {
        propValue.value = val
      },
    })
  }
}
  • 文檔中說起的 「App 級別的 provide」 未在 Vue 2.x 和 @vue/composition-api 中找到實現

🔀 Vue 3.x 中的實現

Vue 3.x beta 中 provide/inject 的簽名和以前 @vue/composition-api 中一致,在此再也不贅述。編輯器

2.1 測試用例

考察文件 packages/runtime-core/__tests__/apiInject.spec.ts:

test 1: 'string keys'

  • 該例測試字符串 key,但一個看點實際上是 setup() 和 functional component 混用狀況
const Provider = {
  setup() {
    provide('foo'1)
    return () => h(Middle)
  }
}
const Middle = {
  render() => h(Consumer)
}
const Consumer = {
  setup() {
    const foo = inject('foo')
    return () => foo
  }
}

test 4: 'nested providers'

  • 在組件多層嵌套關係且有多個同名 key 的 provide() 下,消費者 inject() 到最近一層的值

test 6: 'reactivity with readonly refs'

  • provide() 的 value 爲一個用 readonly() 包裹的 Ref 值
  • 在消費者組件中,對用 reject() 獲得的上述 Ref 值進行操做,不會生效
  • test 8 中對readonly() 包裹的 Reactive 對象屬性操做一樣無效

test 10: 'should not warn when default value is undefined'

  • const foo = inject('foo', undefined) 且 'foo' 未在 provide() 中註冊過的時侯,不該報錯

2.2 調用關係

2.3 部分概括

  • Vue 3.x 中的 provide/inject 是圍繞 vue 實例上的 provides 屬性進行的
  • test 4 中的嵌套關係其實就是在 provide() 時「合併」父組件的 provides 屬性:
// packages/runtime-core/src/apiInject.ts

export function provide<T>(key: InjectionKey<T> | string, value: T{
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    // 若是自身沒有 provides,就直接用父組件的
    // 反之,以父組件的 provides 爲原型建立本身的
    // 這樣在 `inject` 中就能夠簡單地搜索到原型鏈上全部的了
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    provides[key as string] = value
  }
}

而這個 provides 根源上的初始值定義在:

// packages/runtime-core/src/apiCreateApp.ts 

export function createAppContext(): AppContext {
  return {
    ...
    provides: Object.create(null)
  }
}
  • test 10 中的狀況對應於源碼中第一個 else if,直接返回明確傳入的 undefined:
if (key in provides) {
      return provides[key as string]
    } else if (arguments.length > 1) {
      return defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
}
  • 文檔中提到了 「全局 API 更改提案中 App 級別的 provide」:

store 也能夠經過全局 API 更改提案中 App 級別的 provide 來提供,可是消費它的組件中的 useStore 風格的 API 仍是相同的。

An app instance can also provide dependencies that can be injected by any component inside the app. This is similar to using the provide option in a 2.x root instance.

也給出了一個示例:

// in the entry
app.provide({
  [ThemeSymbol]: theme
})

// in a child component
export default {
  inject: {
    theme: {
      from: ThemeSymbol
    }
  },
  template`<div :style="{ color: theme.textColor }" />`
}

vue-next 中實現了這部分邏輯:

// packages/runtime-core/src/apiCreateApp.ts

interface App<HostElement = any> {
  ...
  provide<T>(key: InjectionKey<T> | string, value: T): this
}

...

const app: App = {
    ...
    
    provide(key, value) {
        if (__DEV__ && key in context.provides) {
            warn(
                `App already provides property with key "${String(key)}". ` +
                `It will be overwritten with the new value.`
            )
        }
        // TypeScript doesn't allow symbols as index type
        context.provides[key as string] = value
        
        return app
    }

    ...
}

參考文檔

  • https://composition-api.vuejs.org/zh/#%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91
  • https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md#provide--inject

《Vue 測試指南》中文版

對比 React Hooks 和 Vue Composition API

在 React 和 Vue 中嚐鮮 Hooks



--End--


查看更多前端好文
請搜索 fewelife 關注公衆號

轉載請註明出處



本文分享自微信公衆號 - 雲前端(fewelife)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索