mirror of
https://gitee.com/vuejs/vue.git
synced 2024-12-02 03:57:36 +08:00
wip: provide/inject
This commit is contained in:
parent
dae380dc24
commit
1a13c63ff5
@ -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,17 +56,9 @@ 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]) {
|
||||
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)
|
||||
@ -66,7 +67,6 @@ export function resolveInject(
|
||||
warn(`Injection "${key as string}" not found`, vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.`)
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
336
test/unit/features/v3/apiInject.spec.ts
Normal file
336
test/unit/features/v3/apiInject.spec.ts
Normal 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>`)
|
||||
})
|
||||
})
|
2
typescript/component.d.ts
vendored
2
typescript/component.d.ts
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user