wip: props immutability

This commit is contained in:
Evan You 2019-05-29 10:43:27 +08:00
parent 6dfec3a4ae
commit eac8a4baa3
4 changed files with 81 additions and 55 deletions

View File

@ -1,26 +1,32 @@
import { VNode, normalizeVNode, VNodeChild } from './vnode'
import { ReactiveEffect } from '@vue/observer'
import { isFunction, EMPTY_OBJ } from '@vue/shared'
import { resolveProps, ComponentPropsOptions } from './componentProps'
import { RenderProxyHandlers } from './componentProxy'
import {
resolveProps,
ComponentPropsOptions,
initializeProps,
PropValidator
} from './componentProps'
interface Value<T> {
value: T
}
export type Data = { [key: string]: any }
type UnwrapBindings<T> = {
[key in keyof T]: T[key] extends Value<infer V> ? V : T[key]
}
type Prop<T> = { (): T } | { new (...args: any[]): T & object }
type ExtractPropTypes<PropOptions> = {
readonly [key in keyof PropOptions]: PropOptions[key] extends Prop<infer V>
readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator<
infer V
>
? V
: PropOptions[key] extends null | undefined ? any : PropOptions[key]
}
export type Data = { [key: string]: any }
export interface ComponentPublicProperties<P = Data, S = Data> {
$state: S
$props: P
@ -64,21 +70,6 @@ export type Slots = Readonly<{
[name: string]: Slot
}>
// no-op, for type inference only
export function createComponent<
RawProps,
RawBindings,
Props = ExtractPropTypes<RawProps>,
Bindings = UnwrapBindings<RawBindings>
>(
options: ComponentOptions<RawProps, RawBindings, Props, Bindings>
): {
// for TSX
new (): { $props: Props }
} {
return options as any
}
type LifecycleHook = Function[] | null
export interface LifecycleHooks {
@ -97,22 +88,38 @@ export interface LifecycleHooks {
export type ComponentInstance = {
type: FunctionalComponent | ComponentOptions
vnode: VNode | null
vnode: VNode
next: VNode | null
subTree: VNode | null
subTree: VNode
update: ReactiveEffect
// the rest are only for stateful components
bindings: Data | null
proxy: Data | null
} & LifecycleHooks &
ComponentPublicProperties
export function createComponentInstance(vnode: VNode): ComponentInstance {
const type = vnode.type as any
const instance = {
// no-op, for type inference only
export function createComponent<
RawProps,
RawBindings,
Props = ExtractPropTypes<RawProps>,
Bindings = UnwrapBindings<RawBindings>
>(
options: ComponentOptions<RawProps, RawBindings, Props, Bindings>
): {
// for TSX
new (): { $props: Props }
} {
return options as any
}
export function createComponentInstance(type: any): ComponentInstance {
return {
type,
vnode: null,
vnode: null as any,
next: null,
subTree: null,
subTree: null as any,
update: null as any,
bindings: null,
proxy: null,
@ -136,41 +143,50 @@ export function createComponentInstance(vnode: VNode): ComponentInstance {
$slots: EMPTY_OBJ,
$state: EMPTY_OBJ
}
if (typeof type === 'object' && type.setup) {
setupStatefulComponent(instance)
}
return instance
}
export let currentInstance: ComponentInstance | null = null
const RenderProxyHandlers = {}
export function setupStatefulComponent(instance: ComponentInstance) {
export function setupStatefulComponent(
instance: ComponentInstance,
props: Data | null
) {
const Component = instance.type as ComponentOptions
// 1. create render proxy
const proxy = (instance.proxy = new Proxy(instance, RenderProxyHandlers))
// 2. resolve initial props
initializeProps(instance, Component.props, props)
// 3. call setup()
const type = instance.type as ComponentOptions
if (type.setup) {
if (Component.setup) {
currentInstance = instance
instance.bindings = type.setup.call(proxy, proxy)
instance.bindings = Component.setup.call(proxy, proxy)
currentInstance = null
}
}
export function renderComponentRoot(instance: ComponentInstance): VNode {
export function renderComponentRoot(
instance: ComponentInstance,
useAlreadyResolvedProps?: boolean
): VNode {
const { type, vnode, proxy, bindings, $slots } = instance
if (!type) debugger
const renderArg: RenderFunctionArg = {
state: bindings || EMPTY_OBJ,
slots: $slots,
props: null as any,
attrs: null as any
}
if (useAlreadyResolvedProps) {
// initial render for stateful components with setup()
// props are already resolved
renderArg.props = instance.$props
renderArg.attrs = instance.$attrs
} else {
const { 0: props, 1: attrs } = resolveProps(
(vnode as VNode).props,
type.props
)
const renderArg = {
state: bindings || EMPTY_OBJ,
slots: $slots,
props,
attrs
instance.$props = renderArg.props = props
instance.$attrs = renderArg.attrs = attrs
}
if (isFunction(type)) {
return normalizeVNode(type(renderArg))

View File

@ -45,7 +45,7 @@ const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
export function initializeProps(
instance: ComponentInstance,
options: NormalizedPropsOptions | undefined,
options: ComponentPropsOptions | undefined,
rawProps: Data | null
) {
const { 0: props, 1: attrs } = resolveProps(rawProps, options)

View File

@ -0,0 +1 @@
export const RenderProxyHandlers = {}

View File

@ -11,7 +11,8 @@ import {
ComponentInstance,
renderComponentRoot,
shouldUpdateComponent,
createComponentInstance
createComponentInstance,
setupStatefulComponent
} from './component'
import { isString, isArray, EMPTY_OBJ, EMPTY_ARR } from '@vue/shared'
import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
@ -348,14 +349,22 @@ export function createRenderer(options: RendererOptions) {
container: HostNode,
anchor?: HostNode
) {
const Component = vnode.type
const instance: ComponentInstance = (vnode.component = createComponentInstance(
vnode
Component
))
const needsSetup = typeof Component === 'object' && (Component as any).setup
if (needsSetup) {
setupStatefulComponent(instance, vnode.props)
}
instance.update = effect(() => {
if (!instance.vnode) {
// initial mount
instance.vnode = vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
const subTree = (instance.subTree = renderComponentRoot(
instance,
needsSetup
))
if (instance.bm !== null) {
invokeHooks(instance.bm)
}
@ -373,7 +382,7 @@ export function createRenderer(options: RendererOptions) {
instance.vnode = next
instance.next = null
}
const prevTree = instance.subTree as VNode
const prevTree = instance.subTree
const nextTree = (instance.subTree = renderComponentRoot(instance))
patch(
prevTree,
@ -651,7 +660,7 @@ export function createRenderer(options: RendererOptions) {
function move(vnode: VNode, container: HostNode, anchor: HostNode) {
if (vnode.component != null) {
move(vnode.component.subTree as VNode, container, anchor)
move(vnode.component.subTree, container, anchor)
return
}
if (vnode.type === Fragment) {
@ -671,7 +680,7 @@ export function createRenderer(options: RendererOptions) {
if (instance != null) {
// TODO teardown component
stop(instance.update)
unmount(instance.subTree as VNode, doRemove)
unmount(instance.subTree, doRemove)
if (instance.um !== null) {
queuePostFlushCb(instance.um)
}
@ -702,7 +711,7 @@ export function createRenderer(options: RendererOptions) {
function getNextHostNode(vnode: VNode): HostNode {
return vnode.component === null
? hostNextSibling(vnode.anchor || vnode.el)
: getNextHostNode(vnode.component.subTree as VNode)
: getNextHostNode(vnode.component.subTree)
}
return function render(vnode: VNode, dom: HostNode): VNode {