Skip to content

Commit cb504c2

Browse files
committed
refactor(runtime-core): refactor slots resolution
Get rid of need for setup proxy in production mode and improve console inspection in dev mode
1 parent c5f0f63 commit cb504c2

File tree

5 files changed

+149
-90
lines changed

5 files changed

+149
-90
lines changed

packages/runtime-core/__tests__/hmr.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ describe('hot module replacement', () => {
6868
await nextTick()
6969
expect(serializeInner(root)).toBe(`<div>11</div>`)
7070

71-
// Update text while preserving state
72-
rerender(
73-
parentId,
74-
compileToFunction(
75-
`<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
76-
)
77-
)
78-
expect(serializeInner(root)).toBe(`<div>1!1</div>`)
71+
// // Update text while preserving state
72+
// rerender(
73+
// parentId,
74+
// compileToFunction(
75+
// `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
76+
// )
77+
// )
78+
// expect(serializeInner(root)).toBe(`<div>1!1</div>`)
7979

8080
// Should force child update on slot content change
8181
rerender(

packages/runtime-core/src/component.ts

+42-44
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
exposeRenderContextOnDevProxyTarget
1616
} from './componentProxy'
1717
import { ComponentPropsOptions, initProps } from './componentProps'
18-
import { Slots, resolveSlots } from './componentSlots'
18+
import { Slots, initSlots, InternalSlots } from './componentSlots'
1919
import { warn } from './warning'
2020
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
2121
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
@@ -140,7 +140,7 @@ export interface ComponentInternalInstance {
140140
data: Data
141141
props: Data
142142
attrs: Data
143-
slots: Slots
143+
slots: InternalSlots
144144
proxy: ComponentPublicInstance | null
145145
proxyTarget: ComponentPublicProxyTarget
146146
// alternative proxy used only for runtime-compiled render functions using
@@ -296,7 +296,7 @@ export function setupComponent(
296296
const { props, children, shapeFlag } = instance.vnode
297297
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
298298
initProps(instance, props, isStateful, isSSR)
299-
resolveSlots(instance, children)
299+
initSlots(instance, children)
300300

301301
const setupResult = isStateful
302302
? setupStatefulComponent(instance, isSSR)
@@ -479,56 +479,54 @@ function finishComponentSetup(
479479
}
480480
}
481481

482-
// used to identify a setup context proxy
483-
export const SetupProxySymbol = Symbol()
484-
485-
const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
486-
;['attrs', 'slots'].forEach((type: string) => {
487-
SetupProxyHandlers[type] = {
488-
get: (instance, key) => {
489-
if (__DEV__) {
490-
markAttrsAccessed()
491-
}
492-
// if the user pass the slots proxy to h(), normalizeChildren should not
493-
// attempt to attach ctx to the object
494-
if (key === '_') return 1
495-
return instance[type][key]
496-
},
497-
has: (instance, key) => key === SetupProxySymbol || key in instance[type],
498-
ownKeys: instance => Reflect.ownKeys(instance[type]),
499-
// this is necessary for ownKeys to work properly
500-
getOwnPropertyDescriptor: (instance, key) =>
501-
Reflect.getOwnPropertyDescriptor(instance[type], key),
502-
set: () => false,
503-
deleteProperty: () => false
482+
const slotsHandlers: ProxyHandler<InternalSlots> = {
483+
set: () => {
484+
warn(`setupContext.slots is readonly.`)
485+
return false
486+
},
487+
deleteProperty: () => {
488+
warn(`setupContext.slots is readonly.`)
489+
return false
504490
}
505-
})
491+
}
506492

507-
const attrsProxyHandlers: ProxyHandler<Data> = {
508-
get(target, key: string) {
509-
if (__DEV__) {
510-
markAttrsAccessed()
511-
}
493+
const attrHandlers: ProxyHandler<Data> = {
494+
get: (target, key: string) => {
495+
markAttrsAccessed()
512496
return target[key]
513497
},
514-
set: () => false,
515-
deleteProperty: () => false
498+
set: () => {
499+
warn(`setupContext.attrs is readonly.`)
500+
return false
501+
},
502+
deleteProperty: () => {
503+
warn(`setupContext.attrs is readonly.`)
504+
return false
505+
}
516506
}
517507

518508
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
519-
const context = {
520-
// attrs & slots are non-reactive, but they need to always expose
521-
// the latest values (instance.xxx may get replaced during updates) so we
522-
// need to expose them through a proxy
523-
attrs: __DEV__
524-
? new Proxy(instance.attrs, attrsProxyHandlers)
525-
: instance.attrs,
526-
slots: new Proxy(instance, SetupProxyHandlers.slots),
527-
get emit() {
528-
return instance.emit
509+
if (__DEV__) {
510+
// We use getters in dev in case libs like test-utils overwrite instance
511+
// properties (overwrites should not be done in prod)
512+
return Object.freeze({
513+
get attrs() {
514+
return new Proxy(instance.attrs, attrHandlers)
515+
},
516+
get slots() {
517+
return new Proxy(instance.slots, slotsHandlers)
518+
},
519+
get emit() {
520+
return instance.emit
521+
}
522+
})
523+
} else {
524+
return {
525+
attrs: instance.attrs,
526+
slots: instance.slots,
527+
emit: instance.emit
529528
}
530529
}
531-
return __DEV__ ? Object.freeze(context) : context
532530
}
533531

534532
// record effects created during a component's setup() so that they can be

packages/runtime-core/src/componentSlots.ts

+94-35
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@ import {
33
VNode,
44
VNodeNormalizedChildren,
55
normalizeVNode,
6-
VNodeChild
6+
VNodeChild,
7+
InternalObjectSymbol
78
} from './vnode'
8-
import { isArray, isFunction, EMPTY_OBJ, ShapeFlags } from '@vue/shared'
9+
import {
10+
isArray,
11+
isFunction,
12+
EMPTY_OBJ,
13+
ShapeFlags,
14+
PatchFlags,
15+
extend,
16+
def
17+
} from '@vue/shared'
918
import { warn } from './warning'
1019
import { isKeepAlive } from './components/KeepAlive'
1120
import { withCtx } from './helpers/withRenderContext'
@@ -25,10 +34,12 @@ export type RawSlots = {
2534
// internal, for tracking slot owner instance. This is attached during
2635
// normalizeChildren when the component vnode is created.
2736
_ctx?: ComponentInternalInstance | null
28-
// internal, indicates compiler generated slots = can skip normalization
37+
// internal, indicates compiler generated slots
2938
_?: 1
3039
}
3140

41+
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
42+
3243
const normalizeSlotValue = (value: unknown): VNode[] =>
3344
isArray(value)
3445
? value.map(normalizeVNode)
@@ -50,46 +61,94 @@ const normalizeSlot = (
5061
return normalizeSlotValue(rawSlot(props))
5162
}, ctx)
5263

53-
export function resolveSlots(
64+
const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
65+
const ctx = rawSlots._ctx
66+
for (const key in rawSlots) {
67+
if (isInternalKey(key)) continue
68+
const value = rawSlots[key]
69+
if (isFunction(value)) {
70+
slots[key] = normalizeSlot(key, value, ctx)
71+
} else if (value != null) {
72+
if (__DEV__) {
73+
warn(
74+
`Non-function value encountered for slot "${key}". ` +
75+
`Prefer function slots for better performance.`
76+
)
77+
}
78+
const normalized = normalizeSlotValue(value)
79+
slots[key] = () => normalized
80+
}
81+
}
82+
}
83+
84+
const normalizeVNodeSlots = (
5485
instance: ComponentInternalInstance,
5586
children: VNodeNormalizedChildren
56-
) {
57-
let slots: InternalSlots | void
87+
) => {
88+
if (__DEV__ && !isKeepAlive(instance.vnode)) {
89+
warn(
90+
`Non-function value encountered for default slot. ` +
91+
`Prefer function slots for better performance.`
92+
)
93+
}
94+
const normalized = normalizeSlotValue(children)
95+
instance.slots.default = () => normalized
96+
}
97+
98+
export const initSlots = (
99+
instance: ComponentInternalInstance,
100+
children: VNodeNormalizedChildren
101+
) => {
58102
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
59-
const rawSlots = children as RawSlots
60-
if (rawSlots._ === 1) {
61-
// pre-normalized slots object generated by compiler
62-
slots = children as Slots
103+
if ((children as RawSlots)._ === 1) {
104+
instance.slots = children as InternalSlots
63105
} else {
64-
slots = {}
65-
const ctx = rawSlots._ctx
66-
for (const key in rawSlots) {
67-
if (key === '$stable' || key === '_ctx') continue
68-
const value = rawSlots[key]
69-
if (isFunction(value)) {
70-
slots[key] = normalizeSlot(key, value, ctx)
71-
} else if (value != null) {
72-
if (__DEV__) {
73-
warn(
74-
`Non-function value encountered for slot "${key}". ` +
75-
`Prefer function slots for better performance.`
76-
)
77-
}
78-
const normalized = normalizeSlotValue(value)
79-
slots[key] = () => normalized
80-
}
106+
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
107+
}
108+
} else {
109+
instance.slots = {}
110+
if (children) {
111+
normalizeVNodeSlots(instance, children)
112+
}
113+
}
114+
def(instance.slots, InternalObjectSymbol, true)
115+
}
116+
117+
export const updateSlots = (
118+
instance: ComponentInternalInstance,
119+
children: VNodeNormalizedChildren
120+
) => {
121+
const { vnode, slots } = instance
122+
let needDeletionCheck = true
123+
let deletionComparisonTarget = EMPTY_OBJ
124+
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
125+
if ((children as RawSlots)._ === 1) {
126+
if (!(vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS)) {
127+
// compiled AND static. this means we can skip removal of potential
128+
// stale slots
129+
needDeletionCheck = false
130+
}
131+
// HMR force update
132+
if (__DEV__ && instance.parent && instance.parent.renderUpdated) {
133+
extend(slots, children as Slots)
81134
}
135+
} else {
136+
needDeletionCheck = !(children as RawSlots).$stable
137+
normalizeObjectSlots(children as RawSlots, slots)
82138
}
139+
deletionComparisonTarget = children as RawSlots
83140
} else if (children) {
84141
// non slot object children (direct value) passed to a component
85-
if (__DEV__ && !isKeepAlive(instance.vnode)) {
86-
warn(
87-
`Non-function value encountered for default slot. ` +
88-
`Prefer function slots for better performance.`
89-
)
142+
normalizeVNodeSlots(instance, children)
143+
deletionComparisonTarget = { default: 1 }
144+
}
145+
146+
// delete stale slots
147+
if (needDeletionCheck) {
148+
for (const key in slots) {
149+
if (!isInternalKey(key) && !(key in deletionComparisonTarget)) {
150+
delete slots[key]
151+
}
90152
}
91-
const normalized = normalizeSlotValue(children)
92-
slots = { default: () => normalized }
93153
}
94-
instance.slots = slots || EMPTY_OBJ
95154
}

packages/runtime-core/src/renderer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
} from './scheduler'
4444
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
4545
import { updateProps } from './componentProps'
46-
import { resolveSlots } from './componentSlots'
46+
import { updateSlots } from './componentSlots'
4747
import { pushWarningContext, popWarningContext, warn } from './warning'
4848
import { ComponentPublicInstance } from './componentProxy'
4949
import { createAppAPI, CreateAppFunction } from './apiCreateApp'
@@ -1245,7 +1245,7 @@ function baseCreateRenderer(
12451245
instance.vnode = nextVNode
12461246
instance.next = null
12471247
updateProps(instance, nextVNode.props, optimized)
1248-
resolveSlots(instance, nextVNode.children)
1248+
updateSlots(instance, nextVNode.children)
12491249
}
12501250

12511251
const patchChildren: PatchChildrenFn = (

packages/runtime-core/src/vnode.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,9 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
438438
return
439439
} else {
440440
type = ShapeFlags.SLOTS_CHILDREN
441-
if (!(children as RawSlots)._) {
441+
if (!(children as RawSlots)._ && !(InternalObjectSymbol in children!)) {
442+
// if slots are not normalized, attach context instance
443+
// (compiled / normalized slots already have context)
442444
;(children as RawSlots)._ctx = currentRenderingInstance
443445
}
444446
}

0 commit comments

Comments
 (0)