mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): provide and inject (#158)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
ed6b1718d2
commit
5c9a15140d
|
@ -0,0 +1,397 @@
|
|||
// NOTE: This test is implemented based on the case of `runtime-core/__test__/apiInject.spec.ts`.
|
||||
|
||||
import {
|
||||
type InjectionKey,
|
||||
type Ref,
|
||||
createComponent,
|
||||
createTextNode,
|
||||
createVaporApp,
|
||||
getCurrentInstance,
|
||||
hasInjectionContext,
|
||||
inject,
|
||||
nextTick,
|
||||
provide,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
renderEffect,
|
||||
setText,
|
||||
} from '../src'
|
||||
import { makeRender } from './_utils'
|
||||
|
||||
const define = makeRender<any>()
|
||||
|
||||
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
|
||||
describe('api: provide/inject', () => {
|
||||
it('string keys', () => {
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('foo', 1)
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render() {
|
||||
return createComponent(Consumer)
|
||||
},
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject('foo')
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, foo)
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
})
|
||||
|
||||
it('symbol keys', () => {
|
||||
// also verifies InjectionKey type sync
|
||||
const key: InjectionKey<number> = Symbol()
|
||||
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide(key, 1)
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject(key)
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, foo)
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
})
|
||||
|
||||
it('default values', () => {
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('foo', 'foo')
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(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 (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, foo + bar)
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('foobar')
|
||||
})
|
||||
|
||||
// NOTE: Options API is not supported
|
||||
// it('bound to instance', () => {})
|
||||
|
||||
it('nested providers', () => {
|
||||
const ProviderOne = define({
|
||||
setup() {
|
||||
provide('foo', 'foo')
|
||||
provide('bar', 'bar')
|
||||
return createComponent(ProviderTwo)
|
||||
},
|
||||
})
|
||||
|
||||
const ProviderTwo = {
|
||||
setup() {
|
||||
// override parent value
|
||||
provide('foo', 'fooOverride')
|
||||
provide('baz', 'baz')
|
||||
return createComponent(Consumer)
|
||||
},
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject('foo')
|
||||
const bar = inject('bar')
|
||||
const baz = inject('baz')
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, [foo, bar, baz].join(','))
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
ProviderOne.render()
|
||||
expect(ProviderOne.host.innerHTML).toBe('fooOverride,bar,baz')
|
||||
})
|
||||
|
||||
it('reactivity with refs', async () => {
|
||||
const count = ref(1)
|
||||
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('count', count)
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const count = inject<Ref<number>>('count')!
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
renderEffect(() => {
|
||||
setText(n0, count.value)
|
||||
})
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(Provider.host.innerHTML).toBe('2')
|
||||
})
|
||||
|
||||
it('reactivity with readonly refs', async () => {
|
||||
const count = ref(1)
|
||||
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('count', readonly(count))
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const count = inject<Ref<number>>('count')!
|
||||
// should not work
|
||||
count.value++
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
renderEffect(() => {
|
||||
setText(n0, count.value)
|
||||
})
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
|
||||
expect(
|
||||
`Set operation on key "value" failed: target is readonly`,
|
||||
).toHaveBeenWarned()
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(Provider.host.innerHTML).toBe('2')
|
||||
})
|
||||
|
||||
it('reactivity with objects', async () => {
|
||||
const rootState = reactive({ count: 1 })
|
||||
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('state', rootState)
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const state = inject<typeof rootState>('state')!
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
renderEffect(() => {
|
||||
setText(n0, state.count)
|
||||
})
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
|
||||
rootState.count++
|
||||
await nextTick()
|
||||
expect(Provider.host.innerHTML).toBe('2')
|
||||
})
|
||||
|
||||
it('reactivity with readonly objects', async () => {
|
||||
const rootState = reactive({ count: 1 })
|
||||
|
||||
const Provider = define({
|
||||
setup() {
|
||||
provide('state', readonly(rootState))
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const state = inject<typeof rootState>('state')!
|
||||
// should not work
|
||||
state.count++
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
renderEffect(() => {
|
||||
setText(n0, state.count)
|
||||
})
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('1')
|
||||
|
||||
expect(
|
||||
`Set operation on key "count" failed: target is readonly`,
|
||||
).toHaveBeenWarned()
|
||||
|
||||
rootState.count++
|
||||
await nextTick()
|
||||
expect(Provider.host.innerHTML).toBe('2')
|
||||
})
|
||||
|
||||
it('should warn unfound', () => {
|
||||
const Provider = define({
|
||||
setup() {
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject('foo')
|
||||
expect(foo).toBeUndefined()
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, foo)
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(Provider.host.innerHTML).toBe('')
|
||||
expect(`injection "foo" not found.`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should not warn when default value is undefined', () => {
|
||||
const Provider = define({
|
||||
setup() {
|
||||
return createComponent(Middle)
|
||||
},
|
||||
})
|
||||
|
||||
const Middle = {
|
||||
render: () => createComponent(Consumer),
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject('foo', undefined)
|
||||
return (() => {
|
||||
const n0 = createTextNode()
|
||||
setText(n0, foo)
|
||||
return n0
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
Provider.render()
|
||||
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
// #2400
|
||||
it.todo('should not self-inject', () => {
|
||||
const Comp = define({
|
||||
setup() {
|
||||
provide('foo', 'foo')
|
||||
const injection = inject('foo', null)
|
||||
return () => injection
|
||||
},
|
||||
})
|
||||
|
||||
Comp.render()
|
||||
expect(Comp.host.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
describe('hasInjectionContext', () => {
|
||||
it('should be false outside of setup', () => {
|
||||
expect(hasInjectionContext()).toBe(false)
|
||||
})
|
||||
|
||||
it('should be true within setup', () => {
|
||||
expect.assertions(1)
|
||||
const Comp = define({
|
||||
setup() {
|
||||
expect(hasInjectionContext()).toBe(true)
|
||||
return () => null
|
||||
},
|
||||
})
|
||||
|
||||
Comp.render()
|
||||
})
|
||||
|
||||
it('should be true within app.runWithContext()', () => {
|
||||
expect.assertions(1)
|
||||
createVaporApp({}).runWithContext(() => {
|
||||
expect(hasInjectionContext()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { warn } from './warning'
|
||||
import { version } from '.'
|
||||
import { render, setupComponent, unmountComponent } from './apiRender'
|
||||
import type { InjectionKey } from './apiInject'
|
||||
import type { RawProps } from './componentProps'
|
||||
|
||||
export function createVaporApp(
|
||||
|
@ -22,6 +23,8 @@ export function createVaporApp(
|
|||
let instance: ComponentInternalInstance
|
||||
|
||||
const app: App = {
|
||||
_context: context,
|
||||
|
||||
version,
|
||||
|
||||
get config() {
|
||||
|
@ -38,7 +41,7 @@ export function createVaporApp(
|
|||
|
||||
mount(rootContainer): any {
|
||||
if (!instance) {
|
||||
instance = createComponentInstance(rootComponent, rootProps)
|
||||
instance = createComponentInstance(rootComponent, rootProps, context)
|
||||
setupComponent(instance)
|
||||
render(instance, rootContainer)
|
||||
return instance
|
||||
|
@ -58,18 +61,40 @@ export function createVaporApp(
|
|||
warn(`Cannot unmount an app that is not mounted.`)
|
||||
}
|
||||
},
|
||||
provide(key, value) {
|
||||
if (__DEV__ && (key as string | symbol) in context.provides) {
|
||||
warn(
|
||||
`App already provides property with key "${String(key)}". ` +
|
||||
`It will be overwritten with the new value.`,
|
||||
)
|
||||
}
|
||||
|
||||
context.provides[key as string | symbol] = value
|
||||
|
||||
return app
|
||||
},
|
||||
runWithContext(fn) {
|
||||
const lastApp = currentApp
|
||||
currentApp = app
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
currentApp = lastApp
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
function createAppContext(): AppContext {
|
||||
export function createAppContext(): AppContext {
|
||||
return {
|
||||
app: null as any,
|
||||
config: {
|
||||
errorHandler: undefined,
|
||||
warnHandler: undefined,
|
||||
},
|
||||
provides: Object.create(null),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,6 +107,10 @@ export interface App {
|
|||
isHydrate?: boolean,
|
||||
): ComponentInternalInstance
|
||||
unmount(): void
|
||||
provide<T>(key: string | InjectionKey<T>, value: T): App
|
||||
runWithContext<T>(fn: () => T): T
|
||||
|
||||
_context: AppContext
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
|
@ -100,4 +129,11 @@ export interface AppConfig {
|
|||
export interface AppContext {
|
||||
app: App // for devtools
|
||||
config: AppConfig
|
||||
provides: Record<string | symbol, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Used to identify the current app when using `inject()` within
|
||||
* `app.runWithContext()`.
|
||||
*/
|
||||
export let currentApp: App | null = null
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { isFunction } from '@vue/shared'
|
||||
import { currentInstance } from './component'
|
||||
import { currentApp } from './apiCreateVaporApp'
|
||||
import { warn } from './warning'
|
||||
|
||||
export interface InjectionKey<T> extends Symbol {}
|
||||
|
||||
export function provide<T, K = InjectionKey<T> | string | number>(
|
||||
key: K,
|
||||
value: K extends InjectionKey<infer V> ? V : T,
|
||||
) {
|
||||
if (!currentInstance) {
|
||||
if (__DEV__) {
|
||||
warn(`provide() can only be used inside setup().`)
|
||||
}
|
||||
} else {
|
||||
let provides = currentInstance.provides
|
||||
// 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.provides
|
||||
if (parentProvides === provides) {
|
||||
provides = currentInstance.provides = 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,
|
||||
) {
|
||||
const instance = currentInstance
|
||||
|
||||
// also support looking up from app-level provides w/ `app.runWithContext()`
|
||||
if (instance || currentApp) {
|
||||
// #2400
|
||||
// to support `app.use` plugins,
|
||||
// fallback to appContext's `provides` if the instance is at root
|
||||
const provides = instance
|
||||
? instance.parent == null
|
||||
? instance.appContext && instance.appContext.provides
|
||||
: instance.parent.provides
|
||||
: currentApp!._context.provides
|
||||
|
||||
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 && 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.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `inject()` can be used without warning about being called in the wrong place (e.g. outside of
|
||||
* setup()). This is used by libraries that want to use `inject()` internally without triggering a warning to the end
|
||||
* user. One example is `useRoute()` in `vue-router`.
|
||||
*/
|
||||
export function hasInjectionContext(): boolean {
|
||||
return !!(currentInstance || currentApp)
|
||||
}
|
|
@ -18,9 +18,9 @@ import {
|
|||
normalizeEmitsOptions,
|
||||
} from './componentEmits'
|
||||
import { VaporLifecycleHooks } from './apiLifecycle'
|
||||
|
||||
import type { Data } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { type AppContext, createAppContext } from './apiCreateVaporApp'
|
||||
import type { Data } from '@vue/shared'
|
||||
|
||||
export type Component = FunctionalComponent | ObjectComponent
|
||||
|
||||
|
@ -79,11 +79,13 @@ export interface ComponentInternalInstance {
|
|||
[componentKey]: true
|
||||
uid: number
|
||||
vapor: true
|
||||
appContext: AppContext
|
||||
|
||||
block: Block | null
|
||||
container: ParentNode
|
||||
parent: ComponentInternalInstance | null
|
||||
|
||||
provides: Data
|
||||
scope: EffectScope
|
||||
component: FunctionalComponent | ObjectComponent
|
||||
comps: Set<ComponentInternalInstance>
|
||||
|
@ -180,23 +182,32 @@ export const unsetCurrentInstance = () => {
|
|||
currentInstance = null
|
||||
}
|
||||
|
||||
const emptyAppContext = createAppContext()
|
||||
|
||||
let uid = 0
|
||||
export function createComponentInstance(
|
||||
component: ObjectComponent | FunctionalComponent,
|
||||
rawProps: RawProps | null,
|
||||
// application root node only
|
||||
appContext: AppContext | null = null,
|
||||
): ComponentInternalInstance {
|
||||
const parent = getCurrentInstance()
|
||||
const _appContext =
|
||||
(parent ? parent.appContext : appContext) || emptyAppContext
|
||||
|
||||
const instance: ComponentInternalInstance = {
|
||||
[componentKey]: true,
|
||||
uid: uid++,
|
||||
vapor: true,
|
||||
appContext: _appContext,
|
||||
|
||||
block: null,
|
||||
container: null!,
|
||||
|
||||
// TODO
|
||||
parent: null,
|
||||
parent,
|
||||
|
||||
scope: new EffectScope(true /* detached */)!,
|
||||
provides: parent ? parent.provides : Object.create(_appContext.provides),
|
||||
component,
|
||||
comps: new Set(),
|
||||
dirs: new Map(),
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
export const version = __VERSION__
|
||||
export {
|
||||
// core
|
||||
type Ref,
|
||||
reactive,
|
||||
ref,
|
||||
readonly,
|
||||
|
@ -89,6 +90,12 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
|
|||
export { setRef } from './dom/templateRef'
|
||||
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export {
|
||||
type InjectionKey,
|
||||
inject,
|
||||
provide,
|
||||
hasInjectionContext,
|
||||
} from './apiInject'
|
||||
export {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
|
|
Loading…
Reference in New Issue