refactor: return Proxy from base class constructor

This commit is contained in:
Evan You 2019-03-05 16:24:07 -05:00
parent c335939dcf
commit ec0ccd2337
9 changed files with 62 additions and 56 deletions

View File

@ -1,5 +1,6 @@
import { Component, ComponentClass, mixins } from '@vue/runtime-core' import { Component, ComponentClass, mixins } from '@vue/runtime-core'
import { createInstance } from '@vue/runtime-test' import { createInstance } from '@vue/runtime-test'
import { prop } from '@vue/decorators'
const calls: string[] = [] const calls: string[] = []
@ -9,10 +10,8 @@ beforeEach(() => {
class ClassMixinA extends Component<{ p1: string }, { d11: number }> { class ClassMixinA extends Component<{ p1: string }, { d11: number }> {
// props // props
static props = { @prop
p1: String p1: string
}
// data // data
d1 = 1 d1 = 1
data() { data() {
@ -23,7 +22,7 @@ class ClassMixinA extends Component<{ p1: string }, { d11: number }> {
// computed // computed
get c1() { get c1() {
return this.d1 + this.d11 return this.d1 + this.$data.d11
} }
// lifecycle // lifecycle
@ -52,7 +51,7 @@ class ClassMixinB extends Component<{ p2: string }, { d21: number }> {
} }
get c2() { get c2() {
return this.d2 + this.d21 return this.d2 + this.$data.d21
} }
// lifecycle // lifecycle
@ -197,15 +196,15 @@ describe('mixins', () => {
// data // data
expect(instance.d1).toBe(1) expect(instance.d1).toBe(1)
expect(instance.d11).toBe(2) expect(instance.$data.d11).toBe(2)
expect(instance.d2).toBe(1) expect(instance.d2).toBe(1)
expect(instance.d21).toBe(2) expect(instance.$data.d21).toBe(2)
expect(instance.d3).toBe(1) expect(instance.d3).toBe(1)
expect(instance.d31).toBe(2) expect(instance.d31).toBe(2)
// props // props
expect(instance.p1).toBe('1') expect(instance.p1).toBe('1')
expect(instance.p2).toBe('2') expect(instance.$props.p2).toBe('2')
expect(instance.p3).toBe('3') expect(instance.p3).toBe('3')
expect(instance.$props.p1).toBe('1') expect(instance.$props.p1).toBe('1')
expect(instance.$props.p2).toBe('2') expect(instance.$props.p2).toBe('2')
@ -246,7 +245,7 @@ describe('mixins', () => {
} }
get c3() { get c3() {
return this.d3 + this.d31 return this.d3 + this.$data.d31
} }
created() { created() {
@ -278,7 +277,7 @@ describe('mixins', () => {
} }
get c3() { get c3() {
return this.d3 + this.d31 return this.d3 + this.$data.d31
} }
created() { created() {

View File

@ -14,6 +14,7 @@ import { ErrorTypes } from './errorHandling'
import { initializeComponentInstance } from './componentInstance' import { initializeComponentInstance } from './componentInstance'
import { EventEmitter, invokeListeners } from './optional/eventEmitter' import { EventEmitter, invokeListeners } from './optional/eventEmitter'
import { warn } from './warning' import { warn } from './warning'
import { ComponentProxy } from './componentProxy'
// public component instance type // public component instance type
export interface Component<P = {}, D = {}> extends PublicInstanceMethods { export interface Component<P = {}, D = {}> extends PublicInstanceMethods {
@ -30,7 +31,6 @@ export interface Component<P = {}, D = {}> extends PublicInstanceMethods {
readonly $options: ComponentOptions<P, D, this> readonly $options: ComponentOptions<P, D, this>
readonly $refs: Record<string | symbol, any> readonly $refs: Record<string | symbol, any>
readonly $proxy: this readonly $proxy: this
readonly $self: this
} }
interface PublicInstanceMethods { interface PublicInstanceMethods {
@ -97,10 +97,10 @@ export interface ComponentInstance<P = {}, D = {}>
$props: P $props: P
$attrs: Data $attrs: Data
$slots: Slots $slots: Slots
$root: ComponentInstance $root: ComponentProxy
$children: ComponentInstance[] $children: ComponentProxy[]
$options: ComponentOptions<P, D> $options: ComponentOptions<P, D>
$self: ComponentInstance<P, D> // on proxies only $proxy: ComponentProxy<this>
_update: ReactiveEffect _update: ReactiveEffect
_queueJob: ((fn: () => void) => void) _queueJob: ((fn: () => void) => void)
@ -119,13 +119,12 @@ class ComponentImplementation implements PublicInstanceMethods {
$props: Data | null = null $props: Data | null = null
$attrs: Data | null = null $attrs: Data | null = null
$slots: Slots | null = null $slots: Slots | null = null
$root: ComponentInstance | null = null $root: ComponentProxy | null = null
$parent: ComponentInstance | null = null $parent: ComponentProxy | null = null
$children: ComponentInstance[] = [] $children: ComponentProxy[] = []
$options: ComponentOptions | null = null $options: ComponentOptions | null = null
$refs: Record<string, ComponentInstance | RenderNode> = {} $refs: Record<string, ComponentInstance | RenderNode> = {}
$proxy: any = null $proxy: ComponentProxy<this> | null = null
$self: any
_rawData: Data | null = null _rawData: Data | null = null
_computedGetters: Record<string, ComputedGetter> | null = null _computedGetters: Record<string, ComputedGetter> | null = null
@ -140,7 +139,11 @@ class ComponentImplementation implements PublicInstanceMethods {
constructor(props?: object) { constructor(props?: object) {
if (props === void 0) { if (props === void 0) {
initializeComponentInstance(this as any) // When invoked without any arguments, this is the default path where
// we initiailize a proper component instance. Note the returned value
// here is actually a proxy of the raw instance (and will be the `this`
// context) in all sub-class methods, including the constructor!
return initializeComponentInstance(this as any) as any
} else { } else {
// the presence of the props argument indicates that this class is being // the presence of the props argument indicates that this class is being
// instantiated as a mixin, and should expose the props on itself // instantiated as a mixin, and should expose the props on itself

View File

@ -1,10 +1,10 @@
import { VNode, MountedVNode } from './vdom' import { VNode, MountedVNode } from './vdom'
import { Component, ComponentInstance, ComponentClass } from './component' import { ComponentInstance, ComponentClass } from './component'
import { initializeState } from './componentState' import { initializeState } from './componentState'
import { initializeProps } from './componentProps' import { initializeProps } from './componentProps'
import { initializeWatch, teardownWatch } from './componentWatch' import { initializeWatch, teardownWatch } from './componentWatch'
import { initializeComputed, teardownComputed } from './componentComputed' import { initializeComputed, teardownComputed } from './componentComputed'
import { createRenderProxy } from './componentProxy' import { ComponentProxy, createRenderProxy } from './componentProxy'
import { resolveComponentOptionsFromClass } from './componentOptions' import { resolveComponentOptionsFromClass } from './componentOptions'
import { VNodeFlags } from './flags' import { VNodeFlags } from './flags'
import { ErrorTypes, callLifecycleHookWithHandler } from './errorHandling' import { ErrorTypes, callLifecycleHookWithHandler } from './errorHandling'
@ -14,25 +14,22 @@ import { EMPTY_OBJ } from '@vue/shared'
let currentVNode: VNode | null = null let currentVNode: VNode | null = null
let currentContextVNode: VNode | null = null let currentContextVNode: VNode | null = null
export function createComponentInstance<T extends Component>( export function createComponentInstance(vnode: VNode): ComponentInstance {
vnode: VNode
): ComponentInstance {
// component instance creation is done in two steps. // component instance creation is done in two steps.
// first, `initializeComponentInstance` is called inside base component // first, `initializeComponentInstance` is called inside base component
// constructor as the instance is created so that the extended component's // constructor as the instance is created so that the extended component's
// constructor has access to certain properties and most importantly, // constructor has access to public properties and most importantly props.
// this.$props.
// we are storing the vnodes in variables here so that there's no need to // we are storing the vnodes in variables here so that there's no need to
// always pass args in super() // always pass args in super()
currentVNode = vnode currentVNode = vnode
currentContextVNode = vnode.contextVNode currentContextVNode = vnode.contextVNode
const Component = vnode.tag as ComponentClass const Component = vnode.tag as ComponentClass
const instance = (vnode.children = new Component() as ComponentInstance) const instanceProxy = new Component() as ComponentProxy
const instance = instanceProxy._self
// then we finish the initialization by collecting properties set on the // then we finish the initialization by collecting properties set on the
// instance // instance
const { const {
$proxy,
$options: { created, computed, watch } $options: { created, computed, watch }
} = instance } = instance
initializeState(instance, !Component.fromOptions) initializeState(instance, !Component.fromOptions)
@ -41,7 +38,7 @@ export function createComponentInstance<T extends Component>(
instance.$slots = currentVNode.slots || EMPTY_OBJ instance.$slots = currentVNode.slots || EMPTY_OBJ
if (created) { if (created) {
callLifecycleHookWithHandler(created, $proxy, ErrorTypes.CREATED) callLifecycleHookWithHandler(created, instanceProxy, ErrorTypes.CREATED)
} }
currentVNode = currentContextVNode = null currentVNode = currentContextVNode = null
@ -50,8 +47,11 @@ export function createComponentInstance<T extends Component>(
// this is called inside the base component's constructor // this is called inside the base component's constructor
// it initializes all the way up to props so that they are available // it initializes all the way up to props so that they are available
// inside the extended component's constructor // inside the extended component's constructor, and returns the proxy of the
export function initializeComponentInstance(instance: ComponentInstance) { // raw instance.
export function initializeComponentInstance<T extends ComponentInstance>(
instance: T
): ComponentProxy<T> {
if (__DEV__ && currentVNode === null) { if (__DEV__ && currentVNode === null) {
throw new Error( throw new Error(
`Component classes are not meant to be manually instantiated.` `Component classes are not meant to be manually instantiated.`
@ -88,10 +88,12 @@ export function initializeComponentInstance(instance: ComponentInstance) {
callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE)
} }
initializeProps(instance, props, (currentVNode as VNode).data) initializeProps(instance, props, (currentVNode as VNode).data)
return proxy
} }
export function teardownComponentInstance(instance: ComponentInstance) { export function teardownComponentInstance(instance: ComponentInstance) {
const parentComponent = instance.$parent && instance.$parent.$self const parentComponent = instance.$parent && instance.$parent._self
if (parentComponent && !parentComponent._unmounted) { if (parentComponent && !parentComponent._unmounted) {
parentComponent.$children.splice( parentComponent.$children.splice(
parentComponent.$children.indexOf(instance.$proxy), parentComponent.$children.indexOf(instance.$proxy),

View File

@ -44,13 +44,6 @@ export function initializeProps(
? immutable(attrs) ? immutable(attrs)
: attrs : attrs
: instance.$props : instance.$props
// expose initial props on the raw instance so that they can be accessed
// in the child class constructor by class field initializers.
if (options != null) {
// it's okay to just set it here because props options are normalized
// and reserved keys should have been filtered away
Object.assign(instance, props)
}
} }
// resolve raw VNode data. // resolve raw VNode data.

View File

@ -22,7 +22,7 @@ function getBoundMethod(fn: Function, target: any, receiver: any): Function {
const renderProxyHandlers = { const renderProxyHandlers = {
get(target: ComponentInstance<any, any>, key: string, receiver: any) { get(target: ComponentInstance<any, any>, key: string, receiver: any) {
let i: any let i: any
if (key === '$self') { if (key === '_self') {
return target return target
} else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) { } else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
// data // data
@ -86,6 +86,11 @@ const renderProxyHandlers = {
} }
} }
export function createRenderProxy(instance: any): ComponentInstance { export type ComponentProxy<T = ComponentInstance> = T & { _self: T }
return new Proxy(instance, renderProxyHandlers) as ComponentInstance
export function createRenderProxy<T extends ComponentInstance>(
instance: T
): ComponentProxy<T> {
debugger
return new Proxy(instance, renderProxyHandlers) as any
} }

View File

@ -1249,7 +1249,9 @@ export function createRenderer(options: RendererOptions) {
// a vnode may already have an instance if this is a compat call with // a vnode may already have an instance if this is a compat call with
// new Vue() // new Vue()
const instance = ((__COMPAT__ && vnode.children) || const instance = ((__COMPAT__ && vnode.children) ||
createComponentInstance(vnode as any)) as ComponentInstance (vnode.children = createComponentInstance(
vnode as any
))) as ComponentInstance
// inject platform-specific unmount to keep-alive container // inject platform-specific unmount to keep-alive container
if ((vnode.tag as any)[KeepAliveSymbol] === true) { if ((vnode.tag as any)[KeepAliveSymbol] === true) {

View File

@ -2,6 +2,7 @@ import { ComponentInstance } from './component'
import { warn, pushWarningContext, popWarningContext } from './warning' import { warn, pushWarningContext, popWarningContext } from './warning'
import { VNode } from './vdom' import { VNode } from './vdom'
import { VNodeFlags } from './flags' import { VNodeFlags } from './flags'
import { ComponentProxy } from './componentProxy'
export const enum ErrorTypes { export const enum ErrorTypes {
BEFORE_CREATE = 1, BEFORE_CREATE = 1,
@ -48,7 +49,7 @@ const ErrorTypeStrings: Record<number, string> = {
export function callLifecycleHookWithHandler( export function callLifecycleHookWithHandler(
hook: Function, hook: Function,
instanceProxy: ComponentInstance, instanceProxy: ComponentProxy,
type: ErrorTypes, type: ErrorTypes,
arg?: any arg?: any
) { ) {
@ -56,11 +57,11 @@ export function callLifecycleHookWithHandler(
const res = hook.call(instanceProxy, arg) const res = hook.call(instanceProxy, arg)
if (res && !res._isVue && typeof res.then === 'function') { if (res && !res._isVue && typeof res.then === 'function') {
;(res as Promise<any>).catch(err => { ;(res as Promise<any>).catch(err => {
handleError(err, instanceProxy.$self, type) handleError(err, instanceProxy._self, type)
}) })
} }
} catch (err) { } catch (err) {
handleError(err, instanceProxy.$self, type) handleError(err, instanceProxy._self, type)
} }
} }
@ -85,10 +86,10 @@ export function handleError(
cur = vnode.children as ComponentInstance cur = vnode.children as ComponentInstance
} }
} else if (instance) { } else if (instance) {
cur = (instance as ComponentInstance).$parent const parent = (instance as ComponentInstance).$parent
cur = parent && parent._self
} }
while (cur) { while (cur) {
cur = cur.$self
const handler = cur.errorCaptured const handler = cur.errorCaptured
if (handler) { if (handler) {
try { try {
@ -103,7 +104,7 @@ export function handleError(
logError(err2, ErrorTypes.ERROR_CAPTURED, contextVNode) logError(err2, ErrorTypes.ERROR_CAPTURED, contextVNode)
} }
} }
cur = cur.$parent cur = cur.$parent && cur.$parent._self
} }
logError(err, type, contextVNode) logError(err, type, contextVNode)
} }
@ -118,7 +119,7 @@ function logError(err: Error, type: ErrorTypes, contextVNode: VNode | null) {
warn( warn(
`Private fields cannot be accessed directly on \`this\` in a component ` + `Private fields cannot be accessed directly on \`this\` in a component ` +
`class because they cannot be tunneled through Proxies. ` + `class because they cannot be tunneled through Proxies. ` +
`Use \`this.$self.#field\` instead.` `Use \`this._self.#field\` instead.`
) )
} else { } else {
warn(`Unhandled error${info ? ` ${info}` : ``}`) warn(`Unhandled error${info ? ` ${info}` : ``}`)

View File

@ -17,7 +17,7 @@ class Vue {
// convert it to a class // convert it to a class
const Component = createComponentClassFromOptions(options || {}) const Component = createComponentClassFromOptions(options || {})
const vnode = h(Component) const vnode = h(Component)
const instance = createComponentInstance(vnode) const instance = (vnode.children = createComponentInstance(vnode))
function mount(el: any) { function mount(el: any) {
const dom = typeof el === 'string' ? document.querySelector(el) : el const dom = typeof el === 'string' ? document.querySelector(el) : el
@ -26,10 +26,10 @@ class Vue {
} }
if (options.el) { if (options.el) {
return mount(options.el) return mount(options.el) as any
} else { } else {
;(instance as any).$mount = mount ;(instance as any).$mount = mount
return instance.$proxy return instance.$proxy as any
} }
} }
} }

View File

@ -26,7 +26,8 @@
"@vue/observer": ["packages/observer/src"], "@vue/observer": ["packages/observer/src"],
"@vue/scheduler": ["packages/scheduler/src"], "@vue/scheduler": ["packages/scheduler/src"],
"@vue/compiler-core": ["packages/compiler-core/src"], "@vue/compiler-core": ["packages/compiler-core/src"],
"@vue/server-renderer": ["packages/server-renderer/src"] "@vue/server-renderer": ["packages/server-renderer/src"],
"@vue/decorators": ["packages/decorators/src"]
} }
}, },
"include": [ "include": [