refactor(runtime-core): refactor slots resolution

Get rid of need for setup proxy in production mode and improve console
inspection in dev mode
This commit is contained in:
Evan You 2020-04-06 21:06:48 -04:00
parent c5f0f63b91
commit cb504c287f
5 changed files with 151 additions and 92 deletions

View File

@ -68,14 +68,14 @@ describe('hot module replacement', () => {
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`<div>11</div>`) expect(serializeInner(root)).toBe(`<div>11</div>`)
// Update text while preserving state // // Update text while preserving state
rerender( // rerender(
parentId, // parentId,
compileToFunction( // compileToFunction(
`<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>` // `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
) // )
) // )
expect(serializeInner(root)).toBe(`<div>1!1</div>`) // expect(serializeInner(root)).toBe(`<div>1!1</div>`)
// Should force child update on slot content change // Should force child update on slot content change
rerender( rerender(

View File

@ -15,7 +15,7 @@ import {
exposeRenderContextOnDevProxyTarget exposeRenderContextOnDevProxyTarget
} from './componentProxy' } from './componentProxy'
import { ComponentPropsOptions, initProps } from './componentProps' import { ComponentPropsOptions, initProps } from './componentProps'
import { Slots, resolveSlots } from './componentSlots' import { Slots, initSlots, InternalSlots } from './componentSlots'
import { warn } from './warning' import { warn } from './warning'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { AppContext, createAppContext, AppConfig } from './apiCreateApp' import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
@ -140,7 +140,7 @@ export interface ComponentInternalInstance {
data: Data data: Data
props: Data props: Data
attrs: Data attrs: Data
slots: Slots slots: InternalSlots
proxy: ComponentPublicInstance | null proxy: ComponentPublicInstance | null
proxyTarget: ComponentPublicProxyTarget proxyTarget: ComponentPublicProxyTarget
// alternative proxy used only for runtime-compiled render functions using // alternative proxy used only for runtime-compiled render functions using
@ -296,7 +296,7 @@ export function setupComponent(
const { props, children, shapeFlag } = instance.vnode const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR) initProps(instance, props, isStateful, isSSR)
resolveSlots(instance, children) initSlots(instance, children)
const setupResult = isStateful const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) ? setupStatefulComponent(instance, isSSR)
@ -479,56 +479,54 @@ function finishComponentSetup(
} }
} }
// used to identify a setup context proxy const slotsHandlers: ProxyHandler<InternalSlots> = {
export const SetupProxySymbol = Symbol() set: () => {
warn(`setupContext.slots is readonly.`)
const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {} return false
;['attrs', 'slots'].forEach((type: string) => {
SetupProxyHandlers[type] = {
get: (instance, key) => {
if (__DEV__) {
markAttrsAccessed()
}
// if the user pass the slots proxy to h(), normalizeChildren should not
// attempt to attach ctx to the object
if (key === '_') return 1
return instance[type][key]
}, },
has: (instance, key) => key === SetupProxySymbol || key in instance[type], deleteProperty: () => {
ownKeys: instance => Reflect.ownKeys(instance[type]), warn(`setupContext.slots is readonly.`)
// this is necessary for ownKeys to work properly return false
getOwnPropertyDescriptor: (instance, key) =>
Reflect.getOwnPropertyDescriptor(instance[type], key),
set: () => false,
deleteProperty: () => false
} }
}) }
const attrsProxyHandlers: ProxyHandler<Data> = { const attrHandlers: ProxyHandler<Data> = {
get(target, key: string) { get: (target, key: string) => {
if (__DEV__) {
markAttrsAccessed() markAttrsAccessed()
}
return target[key] return target[key]
}, },
set: () => false, set: () => {
deleteProperty: () => false warn(`setupContext.attrs is readonly.`)
return false
},
deleteProperty: () => {
warn(`setupContext.attrs is readonly.`)
return false
}
} }
function createSetupContext(instance: ComponentInternalInstance): SetupContext { function createSetupContext(instance: ComponentInternalInstance): SetupContext {
const context = { if (__DEV__) {
// attrs & slots are non-reactive, but they need to always expose // We use getters in dev in case libs like test-utils overwrite instance
// the latest values (instance.xxx may get replaced during updates) so we // properties (overwrites should not be done in prod)
// need to expose them through a proxy return Object.freeze({
attrs: __DEV__ get attrs() {
? new Proxy(instance.attrs, attrsProxyHandlers) return new Proxy(instance.attrs, attrHandlers)
: instance.attrs, },
slots: new Proxy(instance, SetupProxyHandlers.slots), get slots() {
return new Proxy(instance.slots, slotsHandlers)
},
get emit() { get emit() {
return instance.emit return instance.emit
} }
})
} else {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
} }
return __DEV__ ? Object.freeze(context) : context
} }
// record effects created during a component's setup() so that they can be // record effects created during a component's setup() so that they can be

View File

@ -3,9 +3,18 @@ import {
VNode, VNode,
VNodeNormalizedChildren, VNodeNormalizedChildren,
normalizeVNode, normalizeVNode,
VNodeChild VNodeChild,
InternalObjectSymbol
} from './vnode' } from './vnode'
import { isArray, isFunction, EMPTY_OBJ, ShapeFlags } from '@vue/shared' import {
isArray,
isFunction,
EMPTY_OBJ,
ShapeFlags,
PatchFlags,
extend,
def
} from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive' import { isKeepAlive } from './components/KeepAlive'
import { withCtx } from './helpers/withRenderContext' import { withCtx } from './helpers/withRenderContext'
@ -25,10 +34,12 @@ export type RawSlots = {
// internal, for tracking slot owner instance. This is attached during // internal, for tracking slot owner instance. This is attached during
// normalizeChildren when the component vnode is created. // normalizeChildren when the component vnode is created.
_ctx?: ComponentInternalInstance | null _ctx?: ComponentInternalInstance | null
// internal, indicates compiler generated slots = can skip normalization // internal, indicates compiler generated slots
_?: 1 _?: 1
} }
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
const normalizeSlotValue = (value: unknown): VNode[] => const normalizeSlotValue = (value: unknown): VNode[] =>
isArray(value) isArray(value)
? value.map(normalizeVNode) ? value.map(normalizeVNode)
@ -50,21 +61,10 @@ const normalizeSlot = (
return normalizeSlotValue(rawSlot(props)) return normalizeSlotValue(rawSlot(props))
}, ctx) }, ctx)
export function resolveSlots( const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) {
let slots: InternalSlots | void
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const rawSlots = children as RawSlots
if (rawSlots._ === 1) {
// pre-normalized slots object generated by compiler
slots = children as Slots
} else {
slots = {}
const ctx = rawSlots._ctx const ctx = rawSlots._ctx
for (const key in rawSlots) { for (const key in rawSlots) {
if (key === '$stable' || key === '_ctx') continue if (isInternalKey(key)) continue
const value = rawSlots[key] const value = rawSlots[key]
if (isFunction(value)) { if (isFunction(value)) {
slots[key] = normalizeSlot(key, value, ctx) slots[key] = normalizeSlot(key, value, ctx)
@ -79,9 +79,12 @@ export function resolveSlots(
slots[key] = () => normalized slots[key] = () => normalized
} }
} }
} }
} else if (children) {
// non slot object children (direct value) passed to a component const normalizeVNodeSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
if (__DEV__ && !isKeepAlive(instance.vnode)) { if (__DEV__ && !isKeepAlive(instance.vnode)) {
warn( warn(
`Non-function value encountered for default slot. ` + `Non-function value encountered for default slot. ` +
@ -89,7 +92,63 @@ export function resolveSlots(
) )
} }
const normalized = normalizeSlotValue(children) const normalized = normalizeSlotValue(children)
slots = { default: () => normalized } instance.slots.default = () => normalized
} }
instance.slots = slots || EMPTY_OBJ
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
if ((children as RawSlots)._ === 1) {
instance.slots = children as InternalSlots
} else {
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
}
} else {
instance.slots = {}
if (children) {
normalizeVNodeSlots(instance, children)
}
}
def(instance.slots, InternalObjectSymbol, true)
}
export const updateSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
const { vnode, slots } = instance
let needDeletionCheck = true
let deletionComparisonTarget = EMPTY_OBJ
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
if ((children as RawSlots)._ === 1) {
if (!(vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS)) {
// compiled AND static. this means we can skip removal of potential
// stale slots
needDeletionCheck = false
}
// HMR force update
if (__DEV__ && instance.parent && instance.parent.renderUpdated) {
extend(slots, children as Slots)
}
} else {
needDeletionCheck = !(children as RawSlots).$stable
normalizeObjectSlots(children as RawSlots, slots)
}
deletionComparisonTarget = children as RawSlots
} else if (children) {
// non slot object children (direct value) passed to a component
normalizeVNodeSlots(instance, children)
deletionComparisonTarget = { default: 1 }
}
// delete stale slots
if (needDeletionCheck) {
for (const key in slots) {
if (!isInternalKey(key) && !(key in deletionComparisonTarget)) {
delete slots[key]
}
}
}
} }

View File

@ -43,7 +43,7 @@ import {
} from './scheduler' } from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
import { updateProps } from './componentProps' import { updateProps } from './componentProps'
import { resolveSlots } from './componentSlots' import { updateSlots } from './componentSlots'
import { pushWarningContext, popWarningContext, warn } from './warning' import { pushWarningContext, popWarningContext, warn } from './warning'
import { ComponentPublicInstance } from './componentProxy' import { ComponentPublicInstance } from './componentProxy'
import { createAppAPI, CreateAppFunction } from './apiCreateApp' import { createAppAPI, CreateAppFunction } from './apiCreateApp'
@ -1245,7 +1245,7 @@ function baseCreateRenderer(
instance.vnode = nextVNode instance.vnode = nextVNode
instance.next = null instance.next = null
updateProps(instance, nextVNode.props, optimized) updateProps(instance, nextVNode.props, optimized)
resolveSlots(instance, nextVNode.children) updateSlots(instance, nextVNode.children)
} }
const patchChildren: PatchChildrenFn = ( const patchChildren: PatchChildrenFn = (

View File

@ -438,7 +438,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
return return
} else { } else {
type = ShapeFlags.SLOTS_CHILDREN type = ShapeFlags.SLOTS_CHILDREN
if (!(children as RawSlots)._) { if (!(children as RawSlots)._ && !(InternalObjectSymbol in children!)) {
// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance ;(children as RawSlots)._ctx = currentRenderingInstance
} }
} }