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:
parent
c5f0f63b91
commit
cb504c287f
@ -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(
|
||||||
|
@ -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] = {
|
deleteProperty: () => {
|
||||||
get: (instance, key) => {
|
warn(`setupContext.slots is readonly.`)
|
||||||
if (__DEV__) {
|
return false
|
||||||
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],
|
|
||||||
ownKeys: instance => Reflect.ownKeys(instance[type]),
|
|
||||||
// this is necessary for ownKeys to work properly
|
|
||||||
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() {
|
||||||
get emit() {
|
return new Proxy(instance.slots, slotsHandlers)
|
||||||
return instance.emit
|
},
|
||||||
|
get 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
|
||||||
|
@ -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,46 +61,94 @@ const normalizeSlot = (
|
|||||||
return normalizeSlotValue(rawSlot(props))
|
return normalizeSlotValue(rawSlot(props))
|
||||||
}, ctx)
|
}, ctx)
|
||||||
|
|
||||||
export function resolveSlots(
|
const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
|
||||||
|
const ctx = rawSlots._ctx
|
||||||
|
for (const key in rawSlots) {
|
||||||
|
if (isInternalKey(key)) continue
|
||||||
|
const value = rawSlots[key]
|
||||||
|
if (isFunction(value)) {
|
||||||
|
slots[key] = normalizeSlot(key, value, ctx)
|
||||||
|
} else if (value != null) {
|
||||||
|
if (__DEV__) {
|
||||||
|
warn(
|
||||||
|
`Non-function value encountered for slot "${key}". ` +
|
||||||
|
`Prefer function slots for better performance.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const normalized = normalizeSlotValue(value)
|
||||||
|
slots[key] = () => normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeVNodeSlots = (
|
||||||
instance: ComponentInternalInstance,
|
instance: ComponentInternalInstance,
|
||||||
children: VNodeNormalizedChildren
|
children: VNodeNormalizedChildren
|
||||||
) {
|
) => {
|
||||||
let slots: InternalSlots | void
|
if (__DEV__ && !isKeepAlive(instance.vnode)) {
|
||||||
|
warn(
|
||||||
|
`Non-function value encountered for default slot. ` +
|
||||||
|
`Prefer function slots for better performance.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const normalized = normalizeSlotValue(children)
|
||||||
|
instance.slots.default = () => normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initSlots = (
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
children: VNodeNormalizedChildren
|
||||||
|
) => {
|
||||||
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
||||||
const rawSlots = children as RawSlots
|
if ((children as RawSlots)._ === 1) {
|
||||||
if (rawSlots._ === 1) {
|
instance.slots = children as InternalSlots
|
||||||
// pre-normalized slots object generated by compiler
|
|
||||||
slots = children as Slots
|
|
||||||
} else {
|
} else {
|
||||||
slots = {}
|
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
|
||||||
const ctx = rawSlots._ctx
|
|
||||||
for (const key in rawSlots) {
|
|
||||||
if (key === '$stable' || key === '_ctx') continue
|
|
||||||
const value = rawSlots[key]
|
|
||||||
if (isFunction(value)) {
|
|
||||||
slots[key] = normalizeSlot(key, value, ctx)
|
|
||||||
} else if (value != null) {
|
|
||||||
if (__DEV__) {
|
|
||||||
warn(
|
|
||||||
`Non-function value encountered for slot "${key}". ` +
|
|
||||||
`Prefer function slots for better performance.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const normalized = normalizeSlotValue(value)
|
|
||||||
slots[key] = () => normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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) {
|
} else if (children) {
|
||||||
// non slot object children (direct value) passed to a component
|
// non slot object children (direct value) passed to a component
|
||||||
if (__DEV__ && !isKeepAlive(instance.vnode)) {
|
normalizeVNodeSlots(instance, children)
|
||||||
warn(
|
deletionComparisonTarget = { default: 1 }
|
||||||
`Non-function value encountered for default slot. ` +
|
}
|
||||||
`Prefer function slots for better performance.`
|
|
||||||
)
|
// delete stale slots
|
||||||
}
|
if (needDeletionCheck) {
|
||||||
const normalized = normalizeSlotValue(children)
|
for (const key in slots) {
|
||||||
slots = { default: () => normalized }
|
if (!isInternalKey(key) && !(key in deletionComparisonTarget)) {
|
||||||
|
delete slots[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
instance.slots = slots || EMPTY_OBJ
|
|
||||||
}
|
}
|
||||||
|
@ -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 = (
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user