refactor(runtime-core): tweak component proxy implementation
This commit is contained in:
parent
d1527fbee4
commit
c97d83aff2
@ -310,7 +310,7 @@ describe('api: createApp', () => {
|
||||
const handler = (app.config.warnHandler = jest.fn(
|
||||
(msg, instance, trace) => {
|
||||
expect(msg).toMatch(`Component is missing template or render function`)
|
||||
expect(instance).toBe(ctx.renderProxy)
|
||||
expect(instance).toBe(ctx.proxy)
|
||||
expect(trace).toMatch(`Hello`)
|
||||
}
|
||||
))
|
||||
|
@ -9,7 +9,7 @@ import { ComponentInternalInstance } from '../src/component'
|
||||
describe('component: proxy', () => {
|
||||
mockWarn()
|
||||
|
||||
it('data', () => {
|
||||
test('data', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
@ -33,7 +33,7 @@ describe('component: proxy', () => {
|
||||
expect(instance!.data.foo).toBe(2)
|
||||
})
|
||||
|
||||
it('renderContext', () => {
|
||||
test('renderContext', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
@ -57,7 +57,7 @@ describe('component: proxy', () => {
|
||||
expect(instance!.renderContext.foo).toBe(2)
|
||||
})
|
||||
|
||||
it('propsProxy', () => {
|
||||
test('propsProxy', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
@ -83,7 +83,7 @@ describe('component: proxy', () => {
|
||||
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('methods', () => {
|
||||
test('public properties', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
@ -111,7 +111,7 @@ describe('component: proxy', () => {
|
||||
expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('sink', async () => {
|
||||
test('sink', async () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
@ -129,4 +129,46 @@ describe('component: proxy', () => {
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
expect(instance!.sink.foo).toBe(1)
|
||||
})
|
||||
|
||||
test('has check', () => {
|
||||
const app = createApp()
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
render() {},
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
foo: 0
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
bar: 1
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'), { msg: 'hello' })
|
||||
|
||||
// props
|
||||
expect('msg' in instanceProxy).toBe(true)
|
||||
// data
|
||||
expect('foo' in instanceProxy).toBe(true)
|
||||
// renderContext
|
||||
expect('bar' in instanceProxy).toBe(true)
|
||||
// public properties
|
||||
expect('$el' in instanceProxy).toBe(true)
|
||||
|
||||
// non-existent
|
||||
expect('$foobar' in instanceProxy).toBe(false)
|
||||
expect('baz' in instanceProxy).toBe(false)
|
||||
|
||||
// set non-existent (goes into sink)
|
||||
instanceProxy.baz = 1
|
||||
expect('baz' in instanceProxy).toBe(true)
|
||||
})
|
||||
})
|
||||
|
@ -18,7 +18,7 @@ describe('directives', () => {
|
||||
function assertBindings(binding: DirectiveBinding) {
|
||||
expect(binding.value).toBe(count.value)
|
||||
expect(binding.arg).toBe('foo')
|
||||
expect(binding.instance).toBe(_instance && _instance.renderProxy)
|
||||
expect(binding.instance).toBe(_instance && _instance.proxy)
|
||||
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ describe('directives', () => {
|
||||
function assertBindings(binding: DirectiveBinding) {
|
||||
expect(binding.value).toBe(count.value)
|
||||
expect(binding.arg).toBe('foo')
|
||||
expect(binding.instance).toBe(_instance && _instance.renderProxy)
|
||||
expect(binding.instance).toBe(_instance && _instance.proxy)
|
||||
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
vnode.appContext = context
|
||||
render(vnode, rootContainer)
|
||||
isMounted = true
|
||||
return vnode.component!.renderProxy
|
||||
return vnode.component!.proxy
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`App has already been mounted. Create a new app instance instead.`
|
||||
|
@ -215,7 +215,7 @@ export function applyOptions(
|
||||
instance.renderContext === EMPTY_OBJ
|
||||
? (instance.renderContext = reactive({}))
|
||||
: instance.renderContext
|
||||
const ctx = instance.renderProxy!
|
||||
const ctx = instance.proxy!
|
||||
const {
|
||||
// composition
|
||||
mixins,
|
||||
|
@ -220,7 +220,7 @@ export function instanceWatch(
|
||||
cb: Function,
|
||||
options?: WatchOptions
|
||||
): StopHandle {
|
||||
const ctx = this.renderProxy as Data
|
||||
const ctx = this.proxy as Data
|
||||
const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
|
||||
const stop = watch(getter, cb.bind(ctx), options)
|
||||
onBeforeUnmount(stop, this)
|
||||
|
@ -2,7 +2,8 @@ import { VNode, VNodeChild, isVNode } from './vnode'
|
||||
import { ReactiveEffect, reactive, shallowReadonly } from '@vue/reactivity'
|
||||
import {
|
||||
PublicInstanceProxyHandlers,
|
||||
ComponentPublicInstance
|
||||
ComponentPublicInstance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
} from './componentProxy'
|
||||
import { ComponentPropsOptions } from './componentProps'
|
||||
import { Slots } from './componentSlots'
|
||||
@ -68,7 +69,10 @@ export interface SetupContext {
|
||||
emit: Emit
|
||||
}
|
||||
|
||||
export type RenderFunction = () => VNodeChild
|
||||
export type RenderFunction = {
|
||||
(): VNodeChild
|
||||
isRuntimeCompiled?: boolean
|
||||
}
|
||||
|
||||
export interface ComponentInternalInstance {
|
||||
type: FunctionalComponent | ComponentOptions
|
||||
@ -82,7 +86,7 @@ export interface ComponentInternalInstance {
|
||||
render: RenderFunction | null
|
||||
effects: ReactiveEffect[] | null
|
||||
provides: Data
|
||||
// cache for renderProxy access type to avoid hasOwnProperty calls
|
||||
// cache for proxy access type to avoid hasOwnProperty calls
|
||||
accessCache: Data | null
|
||||
// cache for render function values that rely on _ctx but won't need updates
|
||||
// after initialized (e.g. inline handlers)
|
||||
@ -98,7 +102,10 @@ export interface ComponentInternalInstance {
|
||||
props: Data
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
renderProxy: ComponentPublicInstance | null
|
||||
proxy: ComponentPublicInstance | null
|
||||
// alternative proxy used only for runtime-compiled render functions using
|
||||
// `with` block
|
||||
withProxy: ComponentPublicInstance | null
|
||||
propsProxy: Data | null
|
||||
setupContext: SetupContext | null
|
||||
refs: Data
|
||||
@ -150,7 +157,8 @@ export function createComponentInstance(
|
||||
subTree: null!, // will be set synchronously right after creation
|
||||
update: null!, // will be set synchronously right after creation
|
||||
render: null,
|
||||
renderProxy: null,
|
||||
proxy: null,
|
||||
withProxy: null,
|
||||
propsProxy: null,
|
||||
setupContext: null,
|
||||
effects: null,
|
||||
@ -264,8 +272,8 @@ export function setupStatefulComponent(
|
||||
}
|
||||
// 0. create render proxy property access cache
|
||||
instance.accessCache = {}
|
||||
// 1. create render proxy
|
||||
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers)
|
||||
// 1. create public instance / render proxy
|
||||
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
|
||||
// 2. create props proxy
|
||||
// the propsProxy is a reactive AND readonly proxy to the actual props.
|
||||
// it will be updated in resolveProps() on updates before render
|
||||
@ -371,6 +379,7 @@ function finishComponentSetup(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (__DEV__ && !Component.render) {
|
||||
/* istanbul ignore if */
|
||||
if (!__RUNTIME_COMPILE__ && Component.template) {
|
||||
@ -387,7 +396,18 @@ function finishComponentSetup(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
instance.render = (Component.render || NOOP) as RenderFunction
|
||||
|
||||
// for runtime-compiled render functions using `with` blocks, the render
|
||||
// proxy used needs a different `has` handler which is more performant and
|
||||
// also only allows a whitelist of globals to fallthrough.
|
||||
if (__RUNTIME_COMPILE__ && instance.render.isRuntimeCompiled) {
|
||||
instance.withProxy = new Proxy(
|
||||
instance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// support for 2.x options
|
||||
|
@ -45,16 +45,25 @@ export type ComponentPublicInstance<
|
||||
ExtractComputedReturns<C> &
|
||||
M
|
||||
|
||||
const publicPropertiesMap = {
|
||||
$data: 'data',
|
||||
$props: 'propsProxy',
|
||||
$attrs: 'attrs',
|
||||
$slots: 'slots',
|
||||
$refs: 'refs',
|
||||
$parent: 'parent',
|
||||
$root: 'root',
|
||||
$emit: 'emit',
|
||||
$options: 'type'
|
||||
const publicPropertiesMap: Record<
|
||||
string,
|
||||
(i: ComponentInternalInstance) => any
|
||||
> = {
|
||||
$: i => i,
|
||||
$el: i => i.vnode.el,
|
||||
$cache: i => i.renderCache,
|
||||
$data: i => i.data,
|
||||
$props: i => i.propsProxy,
|
||||
$attrs: i => i.attrs,
|
||||
$slots: i => i.slots,
|
||||
$refs: i => i.refs,
|
||||
$parent: i => i.parent,
|
||||
$root: i => i.root,
|
||||
$emit: i => i.emit,
|
||||
$options: i => i.type,
|
||||
$forceUpdate: i => i.update,
|
||||
$nextTick: () => nextTick,
|
||||
$watch: i => instanceWatch.bind(i)
|
||||
}
|
||||
|
||||
const enum AccessTypes {
|
||||
@ -78,6 +87,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
type,
|
||||
sink
|
||||
} = target
|
||||
|
||||
// data / props / renderContext
|
||||
// This getter gets called for every property access on the render context
|
||||
// during render and is a major hotspot. The most expensive part of this
|
||||
// is the multiple hasOwn() calls. It's much faster to do a simple property
|
||||
@ -106,31 +117,16 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
}
|
||||
// return the value from propsProxy for ref unwrapping and readonly
|
||||
return propsProxy![key]
|
||||
} else if (key === '$') {
|
||||
// reserved backdoor to access the internal instance
|
||||
return target
|
||||
} else if (key === '$cache') {
|
||||
return target.renderCache || (target.renderCache = [])
|
||||
} else if (key === '$el') {
|
||||
return target.vnode.el
|
||||
} else if (hasOwn(publicPropertiesMap, key)) {
|
||||
}
|
||||
|
||||
// public $xxx properties & user-attached properties (sink)
|
||||
const publicGetter = publicPropertiesMap[key]
|
||||
if (publicGetter !== undefined) {
|
||||
if (__DEV__ && key === '$attrs') {
|
||||
markAttrsAccessed()
|
||||
}
|
||||
return target[publicPropertiesMap[key]]
|
||||
}
|
||||
// methods are only exposed when options are supported
|
||||
if (__FEATURE_OPTIONS__) {
|
||||
switch (key) {
|
||||
case '$forceUpdate':
|
||||
return target.update
|
||||
case '$nextTick':
|
||||
return nextTick
|
||||
case '$watch':
|
||||
return instanceWatch.bind(target)
|
||||
}
|
||||
}
|
||||
if (hasOwn(sink, key)) {
|
||||
return publicGetter(target)
|
||||
} else if (hasOwn(sink, key)) {
|
||||
return sink[key]
|
||||
} else if (__DEV__ && currentRenderingInstance != null) {
|
||||
warn(
|
||||
@ -140,6 +136,18 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
}
|
||||
},
|
||||
|
||||
has(target: ComponentInternalInstance, key: string) {
|
||||
const { data, accessCache, renderContext, type, sink } = target
|
||||
return (
|
||||
accessCache![key] !== undefined ||
|
||||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
|
||||
hasOwn(renderContext, key) ||
|
||||
(type.props != null && hasOwn(type.props, key)) ||
|
||||
hasOwn(publicPropertiesMap, key) ||
|
||||
hasOwn(sink, key)
|
||||
)
|
||||
},
|
||||
|
||||
set(target: ComponentInternalInstance, key: string, value: any): boolean {
|
||||
const { data, renderContext } = target
|
||||
if (data !== EMPTY_OBJ && hasOwn(data, key)) {
|
||||
@ -165,13 +173,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
}
|
||||
}
|
||||
|
||||
if (__RUNTIME_COMPILE__) {
|
||||
// this trap is only called in browser-compiled render functions that use
|
||||
// `with (this) {}`
|
||||
PublicInstanceProxyHandlers.has = (
|
||||
_: ComponentInternalInstance,
|
||||
key: string
|
||||
): boolean => {
|
||||
export const runtimeCompiledRenderProxyHandlers = {
|
||||
...PublicInstanceProxyHandlers,
|
||||
has(_target: ComponentInternalInstance, key: string) {
|
||||
return key[0] !== '_' && !isGloballyWhitelisted(key)
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ export function renderComponentRoot(
|
||||
const {
|
||||
type: Component,
|
||||
vnode,
|
||||
renderProxy,
|
||||
proxy,
|
||||
withProxy,
|
||||
props,
|
||||
slots,
|
||||
attrs,
|
||||
@ -48,7 +49,7 @@ export function renderComponentRoot(
|
||||
}
|
||||
try {
|
||||
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
||||
result = normalizeVNode(instance.render!.call(renderProxy))
|
||||
result = normalizeVNode(instance.render!.call(withProxy || proxy))
|
||||
} else {
|
||||
// functional
|
||||
const render = Component as FunctionalComponent
|
||||
|
@ -113,7 +113,7 @@ export function withDirectives<T extends VNode>(
|
||||
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
|
||||
return vnode
|
||||
}
|
||||
const instance = internalInstance.renderProxy
|
||||
const instance = internalInstance.proxy
|
||||
const props = vnode.props || (vnode.props = {})
|
||||
const bindings = vnode.dirs || (vnode.dirs = new Array(directives.length))
|
||||
const injected: Record<string, true> = {}
|
||||
|
@ -99,7 +99,7 @@ export function handleError(
|
||||
if (instance) {
|
||||
let cur = instance.parent
|
||||
// the exposed instance is the render proxy to keep it consistent with 2.x
|
||||
const exposedInstance = instance.renderProxy
|
||||
const exposedInstance = instance.proxy
|
||||
// in production the hook receives only the error code
|
||||
const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
|
||||
while (cur) {
|
||||
|
@ -840,7 +840,7 @@ export function createRenderer<
|
||||
)
|
||||
popWarningContext()
|
||||
}
|
||||
setRef(n2.ref, n1 && n1.ref, parentComponent, n2.component!.renderProxy)
|
||||
setRef(n2.ref, n1 && n1.ref, parentComponent, n2.component!.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ export function warn(msg: string, ...args: any[]) {
|
||||
ErrorCodes.APP_WARN_HANDLER,
|
||||
[
|
||||
msg + args.join(''),
|
||||
instance && instance.renderProxy,
|
||||
instance && instance.proxy,
|
||||
trace
|
||||
.map(({ vnode }) => `at <${formatComponentName(vnode)}>`)
|
||||
.join('\n'),
|
||||
|
@ -36,7 +36,9 @@ function compileToFunction(
|
||||
...options
|
||||
})
|
||||
|
||||
return new Function('Vue', code)(runtimeDom) as RenderFunction
|
||||
const render = new Function('Vue', code)(runtimeDom) as RenderFunction
|
||||
render.isRuntimeCompiled = true
|
||||
return render
|
||||
}
|
||||
|
||||
registerRuntimeCompiler(compileToFunction)
|
||||
|
Loading…
Reference in New Issue
Block a user