wip: provide/inject

This commit is contained in:
Evan You 2022-05-29 18:43:36 +08:00
parent dae380dc24
commit 1a13c63ff5
6 changed files with 427 additions and 25 deletions

View File

@ -1,12 +1,21 @@
import { hasOwn } from 'shared/util'
import { warn, hasSymbol, isFunction } from '../util/index'
import { defineReactive, toggleObserving } from '../observer/index'
import type { Component } from 'typescript/component'
import { provide } from 'v3/apiInject'
import { setCurrentInstance } from '../../v3/currentInstance'
export function initProvide(vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = isFunction(provide) ? provide.call(vm) : provide
const provideOption = vm.$options.provide
if (provideOption) {
const provided = isFunction(provideOption)
? provideOption.call(vm)
: provideOption
const keys = hasSymbol ? Reflect.ownKeys(provided) : Object.keys(provided)
setCurrentInstance(vm)
for (let i = 0; i < keys.length; i++) {
provide(keys[i], provided[keys[i]])
}
setCurrentInstance()
}
}
@ -47,24 +56,15 @@ export function resolveInject(
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
// @ts-expect-error
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
}
return result

View File

@ -49,6 +49,7 @@ export function initLifecycle(vm: Component) {
vm.$children = []
vm.$refs = {}
vm._provided = parent ? parent._provided : Object.create(null)
vm._watcher = null
vm._inactive = null
vm._directInactive = false

View File

@ -1,3 +1,66 @@
export function provide() {}
import { isFunction, warn } from 'core/util'
import { currentInstance } from './currentInstance'
export function inject() {}
export interface InjectionKey<T> extends Symbol {}
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance._provided
// by default an instance inherits its parent's provides object
// but when it needs to provide values of its own, it creates its
// own provides object using parent provides object as prototype.
// this way in `inject` we can simply look up injections from direct
// parent and let the prototype chain do the work.
const parentProvides =
currentInstance.$parent && currentInstance.$parent._provided
if (parentProvides === provides) {
provides = currentInstance._provided = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T,
treatDefaultAsFactory?: false
): T
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T | (() => T),
treatDefaultAsFactory: true
): T
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides = instance.$parent && instance.$parent._provided
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}

View File

@ -69,6 +69,8 @@ export {
} from 'core/observer/dep'
export { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
export { provide, inject, InjectionKey } from './apiInject'
export { h } from './h'
export { getCurrentInstance } from './currentInstance'
export { useSlots, useAttrs } from './apiSetup'

View File

@ -0,0 +1,336 @@
import Vue from 'vue'
import {
h,
provide,
inject,
InjectionKey,
ref,
nextTick,
Ref,
readonly,
reactive
} from 'v3/index'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
describe('api: provide/inject', () => {
it('string keys', () => {
const Provider = {
setup() {
provide('foo', 1)
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject('foo')
return () => h('div', foo)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>1</div>`)
})
it('symbol keys', () => {
// also verifies InjectionKey type sync
const key: InjectionKey<number> = Symbol()
const Provider = {
setup() {
provide(key, 1)
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject(key) || 1
return () => h('div', foo + 1)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>2</div>`)
})
it('default values', () => {
const Provider = {
setup() {
provide('foo', 'foo')
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
// default value should be ignored if value is provided
const foo = inject('foo', 'fooDefault')
// default value should be used if value is not provided
const bar = inject('bar', 'bar')
return () => h('div', foo + bar)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>foobar</div>`)
})
it('bound to instance', () => {
const Provider = {
setup() {
return () => h(Consumer)
}
}
const Consumer = {
name: 'Consumer',
inject: {
foo: {
from: 'foo',
default() {
return this!.$options.name
}
}
},
render() {
// @ts-ignore
return h('div', this.foo)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>Consumer</div>`)
})
it('nested providers', () => {
const ProviderOne = {
setup() {
provide('foo', 'foo')
provide('bar', 'bar')
return () => h(ProviderTwo)
}
}
const ProviderTwo = {
setup() {
// override parent value
provide('foo', 'fooOverride')
provide('baz', 'baz')
return () => h(Consumer)
}
}
const Consumer = {
setup() {
const foo = inject('foo')
const bar = inject('bar')
const baz = inject('baz')
return () => h('div', [foo, bar, baz].join(','))
}
}
const vm = new Vue(ProviderOne).$mount()
expect(vm.$el.outerHTML).toBe(`<div>fooOverride,bar,baz</div>`)
})
it('reactivity with refs', async () => {
const count = ref(1)
const Provider = {
setup() {
provide('count', count)
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const count = inject<Ref<number>>('count')!
return () => h('div', count.value)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>1</div>`)
count.value++
await nextTick()
expect(vm.$el.outerHTML).toBe(`<div>2</div>`)
})
it('reactivity with readonly refs', async () => {
const count = ref(1)
const Provider = {
setup() {
provide('count', readonly(count))
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const count = inject<Ref<number>>('count')!
// should not work
count.value++
return () => h('div', count.value)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>1</div>`)
expect(
`Set operation on key "value" failed: target is readonly`
).toHaveBeenWarned()
// source mutation should still work
count.value++
await nextTick()
expect(vm.$el.outerHTML).toBe(`<div>2</div>`)
})
it('reactivity with objects', async () => {
const rootState = reactive({ count: 1 })
const Provider = {
setup() {
provide('state', rootState)
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const state = inject<typeof rootState>('state')!
return () => h('div', state.count)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>1</div>`)
rootState.count++
await nextTick()
expect(vm.$el.outerHTML).toBe(`<div>2</div>`)
})
it('reactivity with readonly objects', async () => {
const rootState = reactive({ count: 1 })
const Provider = {
setup() {
provide('state', readonly(rootState))
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const state = inject<typeof rootState>('state')!
// should not work
state.count++
return () => h('div', state.count)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div>1</div>`)
expect(
`Set operation on key "count" failed: target is readonly`
).toHaveBeenWarned()
rootState.count++
await nextTick()
expect(vm.$el.outerHTML).toBe(`<div>2</div>`)
})
it('should warn unfound', () => {
const Provider = {
setup() {
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject('foo')
expect(foo).toBeUndefined()
return () => h('div', foo)
}
}
const vm = new Vue(Provider).$mount()
expect(vm.$el.outerHTML).toBe(`<div></div>`)
expect(`injection "foo" not found.`).toHaveBeenWarned()
})
it('should not warn when default value is undefined', () => {
const Provider = {
setup() {
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject('foo', undefined)
return () => h('div', foo)
}
}
new Vue(Provider).$mount()
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
})
// #2400
it('should not self-inject', () => {
const Comp = {
setup() {
provide('foo', 'foo')
const injection = inject('foo', null)
return () => h('div', injection)
}
}
expect(new Vue(Comp).$mount().$el.outerHTML).toBe(`<div></div>`)
})
})

View File

@ -99,7 +99,7 @@ export declare class Component {
_vnode?: VNode | null // self root node
_staticTrees?: Array<VNode> | null // v-once cached trees
_hasHookEvent: boolean
_provided?: Record<string, any>
_provided: Record<string, any>
// _virtualComponents?: { [key: string]: Component };
// @v3