feat: support defining data in constructor/initialzers

This commit is contained in:
Evan You 2018-10-11 13:54:35 -04:00
parent d9e3ad72c0
commit 60e803ce62
6 changed files with 116 additions and 101 deletions

View File

@ -11,7 +11,7 @@ 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' import { initializeComponentInstance } from './componentUtils'
export interface ComponentClass extends ComponentClassOptions { export interface ComponentClass extends ComponentClassOptions {
options?: ComponentOptions options?: ComponentOptions
@ -101,9 +101,7 @@ class InternalComponent {
public _inactiveRoot: boolean = false public _inactiveRoot: boolean = false
constructor() { constructor() {
this.$options = initializeComponentInstance(this as any)
(this.constructor as ComponentClass).options ||
resolveComponentOptions(this.constructor as ComponentClass)
} }
$nextTick(fn: () => any): Promise<any> { $nextTick(fn: () => any): Promise<any> {

View File

@ -9,6 +9,8 @@ import {
} from './componentOptions' } from './componentOptions'
import { EMPTY_OBJ, camelize, hyphenate, capitalize } from './utils' import { EMPTY_OBJ, camelize, hyphenate, capitalize } from './utils'
const EMPTY_PROPS = { props: EMPTY_OBJ }
export function initializeProps( export function initializeProps(
instance: ComponentInstance, instance: ComponentInstance,
options: ComponentPropsOptions | undefined, options: ComponentPropsOptions | undefined,
@ -19,45 +21,6 @@ export function initializeProps(
instance.$attrs = immutable(attrs || {}) 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. // resolve raw VNode data.
// - filter out reserved keys (key, ref, slots) // - filter out reserved keys (key, ref, slots)
// - extract class and style into $attrs (to be merged onto child // - extract class and style into $attrs (to be merged onto child
@ -125,6 +88,43 @@ export function resolveProps(
return { props, attrs } 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 { const enum BooleanFlags {
shouldCast = '1', shouldCast = '1',
shouldCastTrue = '2' shouldCastTrue = '2'

View File

@ -1,12 +1,21 @@
import { EMPTY_OBJ } from './utils' // import { EMPTY_OBJ } from './utils'
import { ComponentInstance } from './component' import { ComponentInstance } from './component'
import { observable } from '@vue/observer' import { observable } from '@vue/observer'
const internalRE = /^_|^\$/
export function initializeState(instance: ComponentInstance) { export function initializeState(instance: ComponentInstance) {
if (instance.data) { if (instance.data) {
instance._rawData = instance.data() instance._rawData = instance.data()
instance.$data = observable(instance._rawData)
} else { } 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 || {})
}

View File

@ -16,28 +16,62 @@ import { ComponentOptions } from './componentOptions'
import { createRenderProxy } from './componentProxy' import { createRenderProxy } from './componentProxy'
import { handleError, ErrorTypes } from './errorHandling' import { handleError, ErrorTypes } from './errorHandling'
let currentVNode: VNode | null = null
let currentContextVNode: MountedVNode | null = null
export function createComponentInstance( export function createComponentInstance(
vnode: VNode, vnode: VNode,
Component: ComponentClass, Component: ComponentClass,
contextVNode: MountedVNode | null contextVNode: MountedVNode | null
): ComponentInstance { ): 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 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 // renderProxy
const proxy = (instance.$proxy = createRenderProxy(instance)) const proxy = (instance.$proxy = createRenderProxy(instance))
// pointer management // parent chain management
if (contextVNode !== null) { if (currentContextVNode !== null) {
// locate first non-functional parent // locate first non-functional parent
while ( while (
contextVNode !== null && currentContextVNode !== null &&
contextVNode.flags & VNodeFlags.COMPONENT_FUNCTIONAL && currentContextVNode.flags & VNodeFlags.COMPONENT_FUNCTIONAL &&
contextVNode.contextVNode !== null 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 .children as ComponentInstance
instance.$parent = parentComponent.$proxy instance.$parent = parentComponent.$proxy
instance.$root = parentComponent.$root instance.$root = parentComponent.$root
@ -46,20 +80,15 @@ export function createComponentInstance(
instance.$root = proxy instance.$root = proxy
} }
// lifecycle // beforeCreate hook is called right in the constructor
if (instance.beforeCreate) { if (instance.beforeCreate) {
instance.beforeCreate.call(proxy) instance.beforeCreate.call(proxy)
} }
initializeProps(instance, Component.props, vnode.data) initializeProps(
initializeState(instance) instance,
initializeComputed(instance, Component.computed) instance.constructor.props,
initializeWatch(instance, Component.watch) (currentVNode as VNode).data
instance.$slots = vnode.slots || EMPTY_OBJ )
if (instance.created) {
instance.created.call(proxy)
}
return instance as ComponentInstance
} }
export function renderInstanceRoot(instance: ComponentInstance): VNode { export function renderInstanceRoot(instance: ComponentInstance): VNode {
@ -218,11 +247,13 @@ export function createComponentClassFromOptions(
export function resolveComponentOptions( export function resolveComponentOptions(
Component: ComponentClass Component: ComponentClass
): ComponentOptions { ): ComponentOptions {
const keys = Object.keys(Component) const descriptors = Object.getOwnPropertyDescriptors(Component)
const options = {} as any const options = {} as any
for (let i = 0; i < keys.length; i++) { for (const key in descriptors) {
const key = keys[i] const descriptor = descriptors[key]
options[key] = (Component as any)[key] if (descriptor.enumerable || descriptor.get) {
options[key] = descriptor.get ? descriptor.get() : descriptor.value
}
} }
Component.computed = options.computed = resolveComputedOptions(Component) Component.computed = options.computed = resolveComputedOptions(Component)
Component.options = options Component.options = options

View File

@ -18,13 +18,6 @@ interface AsyncComponentFullOptions {
type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions
interface AsyncContainerData {
comp: ComponentType | null
err: Error | null
delayed: boolean
timedOut: boolean
}
export function createAsyncComponent( export function createAsyncComponent(
options: AsyncComponentOptions options: AsyncComponentOptions
): ComponentClass { ): ComponentClass {
@ -40,15 +33,11 @@ export function createAsyncComponent(
error: errorComp error: errorComp
} = options } = options
return class AsyncContainer extends Component<{}, AsyncContainerData> { return class AsyncContainer extends Component {
data() { comp: ComponentType | null = null
return { err: Error | null = null
comp: null, delayed: boolean = false
err: null, timedOut: boolean = false
delayed: false,
timedOut: false
}
}
// doing this in beforeMount so this is non-SSR only // doing this in beforeMount so this is non-SSR only
beforeMount() { beforeMount() {

View File

@ -2,33 +2,21 @@ import {
h, h,
render, render,
nextTick, nextTick,
Component,
createComponentInstance, createComponentInstance,
createComponentClassFromOptions createComponentClassFromOptions
} from '@vue/renderer-dom' } from '@vue/renderer-dom'
// Note: typing for this is intentionally loose, as it will be using 2.x types. class Vue {
class Vue extends Component {
static h = h static h = h
static render = render static render = render
static nextTick = nextTick static nextTick = nextTick
// Note: typing for this is intentionally loose, as it will be using 2.x types.
constructor(options: any) { constructor(options: any) {
super() // convert it to a class
if (!options) { const Component = createComponentClassFromOptions(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)
const vnode = h(Component) const vnode = h(Component)
// the component class is cached on the options object as ._normalized
const instance = createComponentInstance(vnode, Component, null) 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) { function mount(el: any) {
const dom = typeof el === 'string' ? document.querySelector(el) : el const dom = typeof el === 'string' ? document.querySelector(el) : el