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

View File

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

View File

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

View File

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