refactor(runtime-core): tweak component proxy implementation

This commit is contained in:
Evan You 2019-12-10 11:14:29 -05:00
parent d1527fbee4
commit c97d83aff2
14 changed files with 133 additions and 64 deletions

View File

@ -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`)
}
))

View File

@ -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)
})
})

View File

@ -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)
}

View File

@ -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.`

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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> = {}

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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'),

View File

@ -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)