refactor: use flat options on class

This commit is contained in:
Evan You 2018-10-09 20:22:29 -04:00
parent b527705928
commit da6f0d7adc
9 changed files with 92 additions and 102 deletions

View File

@ -95,10 +95,8 @@ describe('attribute fallthrough', () => {
} }
class Child extends Component<{ foo: number }> { class Child extends Component<{ foo: number }> {
static options = { static props = {
props: { foo: Number
foo: Number
}
} }
updated() { updated() {
childUpdated() childUpdated()
@ -169,10 +167,8 @@ describe('attribute fallthrough', () => {
} }
class Child extends Component { class Child extends Component {
static options = { static props = {
props: { foo: Number
foo: Number
}
} }
updated() { updated() {
childUpdated() childUpdated()
@ -183,10 +179,8 @@ describe('attribute fallthrough', () => {
} }
class GrandChild extends Component<{ foo: number }> { class GrandChild extends Component<{ foo: number }> {
static options = { static props = {
props: { foo: Number
foo: Number
}
} }
updated() { updated() {
grandChildUpdated() grandChildUpdated()

View File

@ -3,6 +3,7 @@ import { VNode, Slots, RenderNode, MountedVNode } from './vdom'
import { import {
Data, Data,
ComponentOptions, ComponentOptions,
ComponentClassOptions,
ComponentPropsOptions, ComponentPropsOptions,
WatchOptions WatchOptions
} from './componentOptions' } from './componentOptions'
@ -10,10 +11,10 @@ import { setupWatcher } from './componentWatch'
import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer' import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer'
import { nextTick } from '@vue/scheduler' import { nextTick } from '@vue/scheduler'
import { ErrorTypes } from './errorHandling' import { ErrorTypes } from './errorHandling'
import { resolveComponentOptions } from './componentUtils'
type Flatten<T> = { [K in keyof T]: T[K] } export interface ComponentClass extends ComponentClassOptions {
options?: ComponentOptions
export interface ComponentClass extends Flatten<typeof InternalComponent> {
new <P extends object = {}, D extends object = {}>(): MergedComponent<P, D> new <P extends object = {}, D extends object = {}>(): MergedComponent<P, D>
} }
@ -30,15 +31,15 @@ export interface FunctionalComponent<P = {}> {
export type ComponentType = ComponentClass | FunctionalComponent export type ComponentType = ComponentClass | FunctionalComponent
export interface ComponentInstance<P = {}, D = {}> extends InternalComponent { export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
constructor: ComponentClass
$vnode: MountedVNode $vnode: MountedVNode
$data: D $data: D
$props: Readonly<P> $props: Readonly<P>
$attrs: Data $attrs: Data
$computed: Data
$slots: Slots $slots: Slots
$root: ComponentInstance $root: ComponentInstance
$children: ComponentInstance[] $children: ComponentInstance[]
$options: ComponentOptions<this>
data?(): Partial<D> data?(): Partial<D>
render(props: Readonly<P>, slots: Slots, attrs: Data): any render(props: Readonly<P>, slots: Slots, attrs: Data): any
@ -70,8 +71,6 @@ export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
} }
class InternalComponent { class InternalComponent {
public static options?: ComponentOptions
public get $el(): RenderNode | null { public get $el(): RenderNode | null {
return this.$vnode && this.$vnode.el return this.$vnode && this.$vnode.el
} }
@ -81,12 +80,11 @@ class InternalComponent {
public $data: Data | null = null public $data: Data | null = null
public $props: Data | null = null public $props: Data | null = null
public $attrs: Data | null = null public $attrs: Data | null = null
public $computed: Data | null = null
public $slots: Slots | null = null public $slots: Slots | null = null
public $root: ComponentInstance | null = null public $root: ComponentInstance | null = null
public $parent: ComponentInstance | null = null public $parent: ComponentInstance | null = null
public $children: ComponentInstance[] = [] public $children: ComponentInstance[] = []
public $options: any public $options: ComponentOptions
public $refs: Record<string, ComponentInstance | RenderNode> = {} public $refs: Record<string, ComponentInstance | RenderNode> = {}
public $proxy: any = null public $proxy: any = null
public $forceUpdate: (() => void) | null = null public $forceUpdate: (() => void) | null = null
@ -103,12 +101,10 @@ class InternalComponent {
public _isVue: boolean = true public _isVue: boolean = true
public _inactiveRoot: boolean = false public _inactiveRoot: boolean = false
constructor(options?: ComponentOptions) { constructor() {
this.$options = options || (this.constructor as any).options || EMPTY_OBJ this.$options =
// root instance (this.constructor as ComponentClass).options ||
if (options !== void 0) { resolveComponentOptions(this.constructor as ComponentClass)
// mount this
}
} }
$nextTick(fn: () => any): Promise<any> { $nextTick(fn: () => any): Promise<any> {

View File

@ -1,21 +1,12 @@
import { EMPTY_OBJ, NOOP } from './utils' import { NOOP } from './utils'
import { computed, stop, ComputedGetter } from '@vue/observer' import { computed, stop, ComputedGetter } from '@vue/observer'
import { ComponentClass, ComponentInstance } from './component' import { ComponentClass, ComponentInstance } from './component'
import { ComponentComputedOptions } from './componentOptions' import { ComponentComputedOptions } from './componentOptions'
const extractionCache: WeakMap< export function resolveComputedOptions(
ComponentClass,
ComponentComputedOptions
> = new WeakMap()
export function getComputedOptions(
comp: ComponentClass comp: ComponentClass
): ComponentComputedOptions { ): ComponentComputedOptions {
let computedOptions = extractionCache.get(comp) const computedOptions: ComponentComputedOptions = {}
if (computedOptions) {
return computedOptions
}
computedOptions = {}
const descriptors = Object.getOwnPropertyDescriptors(comp.prototype as any) const descriptors = Object.getOwnPropertyDescriptors(comp.prototype as any)
for (const key in descriptors) { for (const key in descriptors) {
const d = descriptors[key] const d = descriptors[key]
@ -25,7 +16,6 @@ export function getComputedOptions(
// as it's already defined on the prototype // as it's already defined on the prototype
} }
} }
extractionCache.set(comp, computedOptions)
return computedOptions return computedOptions
} }
@ -34,7 +24,6 @@ export function initializeComputed(
computedOptions: ComponentComputedOptions | undefined computedOptions: ComponentComputedOptions | undefined
) { ) {
if (!computedOptions) { if (!computedOptions) {
instance.$computed = EMPTY_OBJ
return return
} }
const handles: Record< const handles: Record<
@ -47,17 +36,6 @@ export function initializeComputed(
const getter = typeof option === 'function' ? option : option.get || NOOP const getter = typeof option === 'function' ? option : option.get || NOOP
handles[key] = computed(getter, proxy) handles[key] = computed(getter, proxy)
} }
instance.$computed = new Proxy(
{},
{
get(_, key: any) {
if (handles.hasOwnProperty(key)) {
return handles[key]()
}
}
// TODO should be readonly
}
)
} }
export function teardownComputed(instance: ComponentInstance) { export function teardownComputed(instance: ComponentInstance) {

View File

@ -3,14 +3,18 @@ import { Slots } from './vdom'
export type Data = Record<string, any> export type Data = Record<string, any>
export interface ComponentOptions<This = ComponentInstance> { export interface ComponentClassOptions<This = ComponentInstance> {
data?(): object
props?: ComponentPropsOptions props?: ComponentPropsOptions
computed?: ComponentComputedOptions<This> computed?: ComponentComputedOptions<This>
watch?: ComponentWatchOptions<This> watch?: ComponentWatchOptions<This>
render?: (this: This, props: Readonly<Data>, slots: Slots, attrs: Data) => any
inheritAttrs?: boolean
displayName?: string displayName?: string
inheritAttrs?: boolean
}
export interface ComponentOptions<This = ComponentInstance>
extends ComponentClassOptions<This> {
data?(): object
render?: (this: This, props: Readonly<Data>, slots: Slots, attrs: Data) => any
// TODO other options // TODO other options
readonly [key: string]: any readonly [key: string]: any
} }

View File

@ -18,9 +18,10 @@ import {
export function initializeProps( export function initializeProps(
instance: ComponentInstance, instance: ComponentInstance,
options: ComponentPropsOptions | undefined,
data: Data | null data: Data | null
) { ) {
const { props, attrs } = resolveProps(data, instance.$options.props) const { props, attrs } = resolveProps(data, options)
instance.$props = immutable(props || {}) instance.$props = immutable(props || {})
instance.$attrs = immutable(attrs || {}) instance.$attrs = immutable(attrs || {})
} }
@ -32,7 +33,7 @@ export function updateProps(instance: ComponentInstance, nextData: Data) {
if (nextData != null) { if (nextData != null) {
const { props: nextProps, attrs: nextAttrs } = resolveProps( const { props: nextProps, attrs: nextAttrs } = resolveProps(
nextData, nextData,
instance.$options.props instance.constructor.props
) )
// unlock to temporarily allow mutatiing props // unlock to temporarily allow mutatiing props
unlock() unlock()

View File

@ -25,8 +25,8 @@ const renderProxyHandlers = {
// data // data
return target.$data[key] return target.$data[key]
} else if ( } else if (
target.$options.props != null && target.constructor.props != null &&
target.$options.props.hasOwnProperty(key) target.constructor.props.hasOwnProperty(key)
) { ) {
// props are only proxied if declared // props are only proxied if declared
return target.$props[key] return target.$props[key]
@ -61,8 +61,8 @@ const renderProxyHandlers = {
return false return false
} }
if ( if (
target.$options.props != null && target.constructor.props != null &&
target.$options.props.hasOwnProperty(key) target.constructor.props.hasOwnProperty(key)
) { ) {
// TODO warn props are immutable // TODO warn props are immutable
return false return false

View File

@ -8,7 +8,7 @@ import { initializeState } from './componentState'
import { initializeProps } from './componentProps' import { initializeProps } from './componentProps'
import { import {
initializeComputed, initializeComputed,
getComputedOptions, resolveComputedOptions,
teardownComputed teardownComputed
} from './componentComputed' } from './componentComputed'
import { initializeWatch, teardownWatch } from './componentWatch' import { initializeWatch, teardownWatch } from './componentWatch'
@ -40,11 +40,10 @@ export function createComponentInstance(
if (instance.beforeCreate) { if (instance.beforeCreate) {
instance.beforeCreate.call(proxy) instance.beforeCreate.call(proxy)
} }
// TODO provide/inject initializeProps(instance, Component.props, vnode.data)
initializeProps(instance, vnode.data)
initializeState(instance) initializeState(instance)
initializeComputed(instance, getComputedOptions(Component)) initializeComputed(instance, Component.computed)
initializeWatch(instance, instance.$options.watch) initializeWatch(instance, Component.watch)
instance.$slots = vnode.slots || EMPTY_OBJ instance.$slots = vnode.slots || EMPTY_OBJ
if (instance.created) { if (instance.created) {
instance.created.call(proxy) instance.created.call(proxy)
@ -75,7 +74,7 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
vnode, vnode,
instance.$parentVNode, instance.$parentVNode,
instance.$attrs, instance.$attrs,
instance.$options.inheritAttrs instance.constructor.inheritAttrs
) )
} }
@ -171,36 +170,40 @@ export function createComponentClassFromOptions(
options: ComponentOptions options: ComponentOptions
): ComponentClass { ): ComponentClass {
class AnonymousComponent extends Component { class AnonymousComponent extends Component {
constructor() { static options = options
super()
this.$options = options
}
} }
const proto = AnonymousComponent.prototype as any const proto = AnonymousComponent.prototype as any
for (const key in options) { for (const key in options) {
const value = options[key] const value = options[key]
// name -> displayName // name -> displayName
if (__COMPAT__ && key === 'name') { if (key === 'name') {
options.displayName = options.name AnonymousComponent.displayName = options.name
} } else if (typeof value === 'function') {
if (typeof value === 'function') { if (__COMPAT__) {
if (__COMPAT__ && key === 'render') { if (key === 'render') {
proto[key] = function() { proto[key] = function() {
return value.call(this, h) return value.call(this, h)
}
} else if (key === 'beforeDestroy') {
proto.beforeUnmount = value
} else if (key === 'destroyed') {
proto.unmounted = value
} }
} else { } else {
proto[key] = value proto[key] = value
} }
} } else if (key === 'computed') {
if (key === 'computed') { AnonymousComponent.computed = value
const isGet = typeof value === 'function' for (const computedKey in value) {
Object.defineProperty(proto, key, { const computed = value[computedKey]
configurable: true, const isGet = typeof computed === 'function'
get: isGet ? value : value.get, Object.defineProperty(proto, computedKey, {
set: isGet ? undefined : value.set configurable: true,
}) get: isGet ? computed : computed.get,
} set: isGet ? undefined : computed.set
if (key === 'methods') { })
}
} else if (key === 'methods') {
for (const method in value) { for (const method in value) {
if (__DEV__ && proto.hasOwnProperty(method)) { if (__DEV__ && proto.hasOwnProperty(method)) {
console.warn( console.warn(
@ -210,7 +213,23 @@ export function createComponentClassFromOptions(
} }
proto[method] = value[method] proto[method] = value[method]
} }
} else {
;(AnonymousComponent as any)[key] = value
} }
} }
return AnonymousComponent as ComponentClass return AnonymousComponent as ComponentClass
} }
export function resolveComponentOptions(
Component: ComponentClass
): ComponentOptions {
const keys = Object.keys(Component)
const options = {} as any
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
options[key] = (Component as any)[key]
}
Component.computed = options.computed = resolveComputedOptions(Component)
Component.options = options
return options
}

View File

@ -9,6 +9,16 @@ interface ProviderProps {
} }
export class Provide extends Component<ProviderProps> { export class Provide extends Component<ProviderProps> {
static props = {
id: {
type: [String, Symbol],
required: true
},
value: {
required: true
}
}
updateValue() { updateValue() {
// TS doesn't allow symbol as index :/ // TS doesn't allow symbol as index :/
// https://github.com/Microsoft/TypeScript/issues/24587 // https://github.com/Microsoft/TypeScript/issues/24587
@ -43,18 +53,6 @@ export class Provide extends Component<ProviderProps> {
} }
} }
Provide.options = {
props: {
id: {
type: [String, Symbol],
required: true
},
value: {
required: true
}
}
}
export class Inject extends Component { export class Inject extends Component {
render(props: any, slots: any) { render(props: any, slots: any) {
return slots.default && slots.default(contextStore[props.id]) return slots.default && slots.default(contextStore[props.id])

View File

@ -114,7 +114,7 @@ export class KeepAlive extends Component<KeepAliveProps> {
;(KeepAlive as any)[KeepAliveSymbol] = true ;(KeepAlive as any)[KeepAliveSymbol] = true
function getName(comp: ComponentClass): string | void { function getName(comp: ComponentClass): string | void {
return comp.options && comp.options.name return comp.displayName || comp.name
} }
function matches(pattern: MatchPattern, name: string): boolean { function matches(pattern: MatchPattern, name: string): boolean {