顺藤摸瓜🍉:用单元测试读懂 vue3 中的 provide/inject

3,582 阅读4分钟

React Context API 提供了一种 Provider 模式,用以在组件树中的多个任意位置的组件之间共享属性,从而避免必须在多层嵌套的结构中层层传递 props。其围绕 Context 的概念,分别提供了 Provider 和 Comsumer 两种对象。

虽然 API 不同,且更倾向用于插件,但 Vue 中同样提供了 Provider 模式。比如 Vue 2.x 文档中对此的描述是:

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。 ...... provide 和 inject 主要在开发高阶插件/组件库时使用。

Vue 3.x 的组合式 API 中也提供了两个类似的独立函数,Composition API RFC 中写道:

许多 Vue 的插件都向 this 注入 property ...... 当使用组合式 API 时,我们不再使用 this,取而代之的是,插件将在内部利用 provide 和 inject 并暴露一个组合函数。

延续系列的主题,本文将继续尝试立足于相关模块的单元测试解读和适度源码分析,主要考察 Vue 3.x Composition API 中的 provide() 和 inject() 两个方法;希望能在结合阅读文档的基础上,更好地理解相关模块。

我们将要观察三个代码仓库,分别是:

  • 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<T> extends 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 调用关系

简单分析源码,主要函数的调用关系为:

1.4 部分归纳

  • 核心部分仍是 Vue 2.x 中已经实现的 vm._provided 内部对象
    • 和原有的 Options API 中的 provide/inject 属性达到统一处理的效果
    • inject() 只能在 setup() 或 functional component 中使用
  • 在用例 test 3、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
    }

    ...
}

参考文档

《Vue 测试指南》中文版

对比 React Hooks 和 Vue Composition API

在 React 和 Vue 中尝鲜 Hooks



--End--

查看更多前端好文
请搜索 fewelife 关注公众号

转载请注明出处