fix: provide/inject should be resolved in parent tree

This commit is contained in:
Evan You 2018-10-15 13:12:13 -04:00
parent e4e138197c
commit d4cd3fb352
4 changed files with 67 additions and 29 deletions

View File

@ -46,7 +46,7 @@ interface PublicInstanceMethods {
interface APIMethods<P, D> { interface APIMethods<P, D> {
data?(): Partial<D> data?(): Partial<D>
render(props: Readonly<P>, slots: Slots, attrs: Data): any render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any
} }
interface LifecycleMethods { interface LifecycleMethods {
@ -76,7 +76,7 @@ export interface ComponentClass extends ComponentClassOptions {
} }
export interface FunctionalComponent<P = {}> { export interface FunctionalComponent<P = {}> {
(props: P, slots: Slots, attrs: Data): any (props: P, slots: Slots, attrs: Data, parentVNode: VNode): any
pure?: boolean pure?: boolean
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
displayName?: string displayName?: string

View File

@ -107,7 +107,8 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
instance.$proxy, instance.$proxy,
instance.$props, instance.$props,
instance.$slots, instance.$slots,
instance.$attrs instance.$attrs,
instance.$parentVNode
) )
} catch (err) { } catch (err) {
handleError(err, instance, ErrorTypes.RENDER) handleError(err, instance, ErrorTypes.RENDER)
@ -120,7 +121,7 @@ export function renderFunctionalRoot(vnode: VNode): VNode {
const { props, attrs } = resolveProps(vnode.data, render.props) const { props, attrs } = resolveProps(vnode.data, render.props)
let subTree let subTree
try { try {
subTree = render(props, vnode.slots || EMPTY_OBJ, attrs || EMPTY_OBJ) subTree = render(props, vnode.slots || EMPTY_OBJ, attrs || EMPTY_OBJ, vnode)
} catch (err) { } catch (err) {
handleError(err, vnode, ErrorTypes.RENDER) handleError(err, vnode, ErrorTypes.RENDER)
} }

View File

@ -1,8 +1,8 @@
import { observable } from '@vue/observer' import { observable } from '@vue/observer'
import { Component } from '../component' import { Component, FunctionalComponent, ComponentInstance } from '../component'
import { warn } from '../warning' import { warn } from '../warning'
import { Slots, VNode } from '../vdom'
const contextStore = observable() as Record<string | symbol, any> import { VNodeFlags } from '../flags'
interface ProviderProps { interface ProviderProps {
id: string | symbol id: string | symbol
@ -10,6 +10,8 @@ interface ProviderProps {
} }
export class Provide extends Component<ProviderProps> { export class Provide extends Component<ProviderProps> {
context: Record<string | symbol, any> = observable()
static props = { static props = {
id: { id: {
type: [String, Symbol], type: [String, Symbol],
@ -20,40 +22,75 @@ export class Provide extends Component<ProviderProps> {
} }
} }
updateValue() {
// TS doesn't allow symbol as index :/
// https://github.com/Microsoft/TypeScript/issues/24587
contextStore[this.$props.id as string] = this.$props.value
}
created() { created() {
if (__DEV__) { const { $props, context } = this
const { id } = this.$props this.$watch(
if (contextStore.hasOwnProperty(id)) { () => $props.value,
warn(`A context provider with id ${id.toString()} already exists.`) value => {
// TS doesn't allow symbol as index :/
// https://github.com/Microsoft/TypeScript/issues/24587
context[$props.id as string] = value
},
{
sync: true,
immediate: true
} }
)
if (__DEV__) {
this.$watch( this.$watch(
() => this.$props.id, () => $props.id,
(id: string, oldId: string) => { (id, oldId) => {
warn( warn(
`Context provider id change detected (from "${oldId}" to "${id}"). ` + `Context provider id change detected (from "${oldId}" to "${id}"). ` +
`This is not supported and should be avoided.` `This is not supported and should be avoided as it leads to ` +
`indeterministic context resolution.`
) )
}, },
{ sync: true } {
sync: true
}
) )
} }
this.updateValue()
}
beforeUpdate() {
this.updateValue()
} }
render(props: any, slots: any) { render(props: any, slots: any) {
return slots.default && slots.default() return slots.default && slots.default()
} }
} }
export class Inject extends Component { interface InjectProps {
render(props: any, slots: any) { id: string | symbol
return slots.default && slots.default(contextStore[props.id])
}
} }
export const Inject: FunctionalComponent<InjectProps> = (
props: InjectProps,
slots: Slots,
attrs: any,
vnode: VNode
) => {
let resolvedValue
let resolved = false
const { id } = props
// walk the parent chain to locate context with key
while (vnode !== null && vnode.contextVNode !== null) {
if (
vnode.flags & VNodeFlags.COMPONENT_STATEFUL &&
(vnode.children as ComponentInstance).constructor === Provide &&
(vnode.children as any).context.hasOwnProperty(id)
) {
resolved = true
resolvedValue = (vnode.children as any).context[id]
break
}
vnode = vnode.contextVNode
}
if (__DEV__ && !resolved) {
warn(
`Inject with id "${id.toString()}" did not match any Provider with ` +
`corresponding property in the parent chain.`
)
}
return slots.default && slots.default(resolvedValue)
}
Inject.pure = true

View File

@ -14,7 +14,7 @@ export function popWarningContext() {
export function warn(msg: string, ...args: any[]) { export function warn(msg: string, ...args: any[]) {
// TODO warn handler? // TODO warn handler?
warn(`[Vue warn]: ${msg}${getComponentTrace()}`, ...args) console.warn(`[Vue warn]: ${msg}${getComponentTrace()}`, ...args)
} }
function getComponentTrace(): string { function getComponentTrace(): string {