From 60e803ce629858ff03cf630aef4861d9adf831f4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 11 Oct 2018 13:54:35 -0400 Subject: [PATCH] feat: support defining data in constructor/initialzers --- packages/core/src/component.ts | 6 +- packages/core/src/componentProps.ts | 78 ++++++++++---------- packages/core/src/componentState.ts | 15 +++- packages/core/src/componentUtils.ts | 77 +++++++++++++------ packages/core/src/optional/asyncComponent.ts | 21 ++---- packages/vue/src/index.ts | 20 +---- 6 files changed, 116 insertions(+), 101 deletions(-) diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index ec5254d4..41997f36 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -11,7 +11,7 @@ import { setupWatcher } from './componentWatch' import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer' import { nextTick } from '@vue/scheduler' import { ErrorTypes } from './errorHandling' -import { resolveComponentOptions } from './componentUtils' +import { initializeComponentInstance } from './componentUtils' export interface ComponentClass extends ComponentClassOptions { options?: ComponentOptions @@ -101,9 +101,7 @@ class InternalComponent { public _inactiveRoot: boolean = false constructor() { - this.$options = - (this.constructor as ComponentClass).options || - resolveComponentOptions(this.constructor as ComponentClass) + initializeComponentInstance(this as any) } $nextTick(fn: () => any): Promise { diff --git a/packages/core/src/componentProps.ts b/packages/core/src/componentProps.ts index e10d6a95..d8618acb 100644 --- a/packages/core/src/componentProps.ts +++ b/packages/core/src/componentProps.ts @@ -9,6 +9,8 @@ import { } from './componentOptions' import { EMPTY_OBJ, camelize, hyphenate, capitalize } from './utils' +const EMPTY_PROPS = { props: EMPTY_OBJ } + export function initializeProps( instance: ComponentInstance, options: ComponentPropsOptions | undefined, @@ -19,45 +21,6 @@ export function initializeProps( instance.$attrs = immutable(attrs || {}) } -export function updateProps(instance: ComponentInstance, nextData: Data) { - // instance.$props and instance.$attrs are observables that should not be - // replaced. Instead, we mutate them to match latest props, which will trigger - // updates if any value that's been used in child component has changed. - if (nextData != null) { - const { props: nextProps, attrs: nextAttrs } = resolveProps( - nextData, - instance.constructor.props - ) - // unlock to temporarily allow mutatiing props - unlock() - const props = instance.$props - const rawProps = unwrap(props) - for (const key in rawProps) { - if (!nextProps.hasOwnProperty(key)) { - delete (props as any)[key] - } - } - for (const key in nextProps) { - ;(props as any)[key] = nextProps[key] - } - if (nextAttrs) { - const attrs = instance.$attrs - const rawAttrs = unwrap(attrs) - for (const key in rawAttrs) { - if (!nextAttrs.hasOwnProperty(key)) { - delete attrs[key] - } - } - for (const key in nextAttrs) { - attrs[key] = nextAttrs[key] - } - } - lock() - } -} - -const EMPTY_PROPS = { props: EMPTY_OBJ } - // resolve raw VNode data. // - filter out reserved keys (key, ref, slots) // - extract class and style into $attrs (to be merged onto child @@ -125,6 +88,43 @@ export function resolveProps( return { props, attrs } } +export function updateProps(instance: ComponentInstance, nextData: Data) { + // instance.$props and instance.$attrs are observables that should not be + // replaced. Instead, we mutate them to match latest props, which will trigger + // updates if any value that's been used in child component has changed. + if (nextData != null) { + const { props: nextProps, attrs: nextAttrs } = resolveProps( + nextData, + instance.constructor.props + ) + // unlock to temporarily allow mutatiing props + unlock() + const props = instance.$props + const rawProps = unwrap(props) + for (const key in rawProps) { + if (!nextProps.hasOwnProperty(key)) { + delete (props as any)[key] + } + } + for (const key in nextProps) { + ;(props as any)[key] = nextProps[key] + } + if (nextAttrs) { + const attrs = instance.$attrs + const rawAttrs = unwrap(attrs) + for (const key in rawAttrs) { + if (!nextAttrs.hasOwnProperty(key)) { + delete attrs[key] + } + } + for (const key in nextAttrs) { + attrs[key] = nextAttrs[key] + } + } + lock() + } +} + const enum BooleanFlags { shouldCast = '1', shouldCastTrue = '2' diff --git a/packages/core/src/componentState.ts b/packages/core/src/componentState.ts index 114f8113..931794f4 100644 --- a/packages/core/src/componentState.ts +++ b/packages/core/src/componentState.ts @@ -1,12 +1,21 @@ -import { EMPTY_OBJ } from './utils' +// import { EMPTY_OBJ } from './utils' import { ComponentInstance } from './component' import { observable } from '@vue/observer' +const internalRE = /^_|^\$/ + export function initializeState(instance: ComponentInstance) { if (instance.data) { instance._rawData = instance.data() - instance.$data = observable(instance._rawData) } else { - instance.$data = EMPTY_OBJ + const keys = Object.keys(instance) + const data = (instance._rawData = {} as any) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + if (!internalRE.test(key)) { + data[key] = (instance as any)[key] + } + } } + instance.$data = observable(instance._rawData || {}) } diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index 8f9ab8b6..a838f3f6 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -16,28 +16,62 @@ import { ComponentOptions } from './componentOptions' import { createRenderProxy } from './componentProxy' import { handleError, ErrorTypes } from './errorHandling' +let currentVNode: VNode | null = null +let currentContextVNode: MountedVNode | null = null + export function createComponentInstance( vnode: VNode, Component: ComponentClass, contextVNode: MountedVNode | null ): ComponentInstance { + // component instance creation is done in two steps. + // first, `initializeComponentInstance` is called inside base component + // constructor as the instance is created so that + currentVNode = vnode + currentContextVNode = contextVNode const instance = (vnode.children = new Component()) as ComponentInstance - instance.$parentVNode = vnode as MountedVNode + // then we finish the initialization by collecting properties set on the + // instance + initializeState(instance) + initializeComputed(instance, Component.computed) + initializeWatch(instance, Component.watch) + instance.$slots = currentVNode.slots || EMPTY_OBJ + if (instance.created) { + instance.created.call(instance.$proxy) + } + currentVNode = currentContextVNode = null + return instance +} + +// this is called inside the base component's constructor +// it initializes all the way up to props so that they are available +// inside the extended component's constructor +export function initializeComponentInstance(instance: ComponentInstance) { + if (__DEV__ && currentVNode === null) { + throw new Error( + `Component classes are not meant to be manually instantiated.` + ) + } + + instance.$options = + instance.constructor.options || + resolveComponentOptions(instance.constructor) + instance.$parentVNode = currentVNode as MountedVNode // renderProxy const proxy = (instance.$proxy = createRenderProxy(instance)) - // pointer management - if (contextVNode !== null) { + // parent chain management + if (currentContextVNode !== null) { // locate first non-functional parent while ( - contextVNode !== null && - contextVNode.flags & VNodeFlags.COMPONENT_FUNCTIONAL && - contextVNode.contextVNode !== null + currentContextVNode !== null && + currentContextVNode.flags & VNodeFlags.COMPONENT_FUNCTIONAL && + currentContextVNode.contextVNode !== null ) { - contextVNode = contextVNode.contextVNode as any + currentContextVNode = currentContextVNode.contextVNode as any } - const parentComponent = (contextVNode as VNode) + const parentComponent = (currentContextVNode as VNode) .children as ComponentInstance instance.$parent = parentComponent.$proxy instance.$root = parentComponent.$root @@ -46,20 +80,15 @@ export function createComponentInstance( instance.$root = proxy } - // lifecycle + // beforeCreate hook is called right in the constructor if (instance.beforeCreate) { instance.beforeCreate.call(proxy) } - initializeProps(instance, Component.props, vnode.data) - initializeState(instance) - initializeComputed(instance, Component.computed) - initializeWatch(instance, Component.watch) - instance.$slots = vnode.slots || EMPTY_OBJ - if (instance.created) { - instance.created.call(proxy) - } - - return instance as ComponentInstance + initializeProps( + instance, + instance.constructor.props, + (currentVNode as VNode).data + ) } export function renderInstanceRoot(instance: ComponentInstance): VNode { @@ -218,11 +247,13 @@ export function createComponentClassFromOptions( export function resolveComponentOptions( Component: ComponentClass ): ComponentOptions { - const keys = Object.keys(Component) + const descriptors = Object.getOwnPropertyDescriptors(Component) const options = {} as any - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - options[key] = (Component as any)[key] + for (const key in descriptors) { + const descriptor = descriptors[key] + if (descriptor.enumerable || descriptor.get) { + options[key] = descriptor.get ? descriptor.get() : descriptor.value + } } Component.computed = options.computed = resolveComputedOptions(Component) Component.options = options diff --git a/packages/core/src/optional/asyncComponent.ts b/packages/core/src/optional/asyncComponent.ts index 5b5a2c70..e0a6d0d4 100644 --- a/packages/core/src/optional/asyncComponent.ts +++ b/packages/core/src/optional/asyncComponent.ts @@ -18,13 +18,6 @@ interface AsyncComponentFullOptions { type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions -interface AsyncContainerData { - comp: ComponentType | null - err: Error | null - delayed: boolean - timedOut: boolean -} - export function createAsyncComponent( options: AsyncComponentOptions ): ComponentClass { @@ -40,15 +33,11 @@ export function createAsyncComponent( error: errorComp } = options - return class AsyncContainer extends Component<{}, AsyncContainerData> { - data() { - return { - comp: null, - err: null, - delayed: false, - timedOut: false - } - } + return class AsyncContainer extends Component { + comp: ComponentType | null = null + err: Error | null = null + delayed: boolean = false + timedOut: boolean = false // doing this in beforeMount so this is non-SSR only beforeMount() { diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 7fed1d2f..7bcae758 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -2,33 +2,21 @@ import { h, render, nextTick, - Component, createComponentInstance, createComponentClassFromOptions } from '@vue/renderer-dom' -// Note: typing for this is intentionally loose, as it will be using 2.x types. -class Vue extends Component { +class Vue { static h = h static render = render static nextTick = nextTick + // Note: typing for this is intentionally loose, as it will be using 2.x types. constructor(options: any) { - super() - if (!options) { - return - } - - // in compat mode, h() can take an options object and will convert it - // to a 3.x class-based component. - const Component = createComponentClassFromOptions(options) + // convert it to a class + const Component = createComponentClassFromOptions(options || {}) const vnode = h(Component) - // the component class is cached on the options object as ._normalized const instance = createComponentInstance(vnode, Component, null) - // set the instance on the vnode before mounting. - // the mount function will skip creating a new instance if it finds an - // existing one. - vnode.children = instance function mount(el: any) { const dom = typeof el === 'string' ? document.querySelector(el) : el