顺藤摸瓜:用单元测试读懂 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源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索