init (graduate from prototype)

This commit is contained in:
Evan You
2018-09-19 11:35:38 -04:00
commit 3401f6b460
63 changed files with 8372 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
import { EMPTY_OBJ } from './utils'
import { VNode, Slots, RenderNode, RenderFragment } from './vdom'
import {
Data,
RenderFunction,
ComponentOptions,
ComponentPropsOptions
} from './componentOptions'
import { setupWatcher } from './componentWatch'
import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer'
type Flatten<T> = { [K in keyof T]: T[K] }
export interface ComponentClass extends Flatten<typeof Component> {
new <D = Data, P = Data>(): MountedComponent<D, P> & D & P
}
export interface FunctionalComponent<P = Data> extends RenderFunction<P> {
pure?: boolean
props?: ComponentPropsOptions<P>
}
// this interface is merged with the class type
// to represent a mounted component
export interface MountedComponent<D = Data, P = Data> extends Component {
$vnode: VNode
$data: D
$props: P
$computed: Data
$slots: Slots
$root: MountedComponent
$children: MountedComponent[]
$options: ComponentOptions<D, P>
render: RenderFunction<P>
data?(): Partial<D>
beforeCreate?(): void
created?(): void
beforeMount?(): void
mounted?(): void
beforeUpdate?(e: DebuggerEvent): void
updated?(): void
beforeDestroy?(): void
destroyed?(): void
_updateHandle: Autorun
$forceUpdate: () => void
_self: MountedComponent<D, P> // on proxies only
}
export class Component {
public static options?: ComponentOptions
public get $el(): RenderNode | RenderFragment | null {
return this.$vnode && this.$vnode.el
}
public $vnode: VNode | null = null
public $parentVNode: VNode | null = null
public $data: Data | null = null
public $props: Data | null = null
public $computed: Data | null = null
public $slots: Slots | null = null
public $root: MountedComponent | null = null
public $parent: MountedComponent | null = null
public $children: MountedComponent[] = []
public $options: any
public $proxy: any = null
public $forceUpdate: (() => void) | null = null
public _rawData: Data | null = null
public _computedGetters: Record<string, ComputedGetter> | null = null
public _watchHandles: Set<Autorun> | null = null
public _mounted: boolean = false
public _destroyed: boolean = false
public _events: { [event: string]: Function[] | null } | null = null
public _updateHandle: Autorun | null = null
public _revokeProxy: () => void
public _isVue: boolean = true
constructor(options?: ComponentOptions) {
this.$options = options || (this.constructor as any).options || EMPTY_OBJ
// root instance
if (options !== void 0) {
// mount this
}
}
$watch(
this: MountedComponent,
keyOrFn: string | (() => any),
cb: () => void
) {
return setupWatcher(this, keyOrFn, cb)
}
// eventEmitter interface
$on(event: string, fn: Function): Component {
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
this.$on(event[i], fn)
}
} else {
const events = this._events || (this._events = Object.create(null))
;(events[event] || (events[event] = [])).push(fn)
}
return this
}
$once(event: string, fn: Function): Component {
const onceFn = (...args: any[]) => {
this.$off(event, onceFn)
fn.apply(this, args)
}
;(onceFn as any).fn = fn
return this.$on(event, onceFn)
}
$off(event?: string, fn?: Function) {
if (this._events) {
if (!event && !fn) {
this._events = null
} else if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
this.$off(event[i], fn)
}
} else if (!fn) {
this._events[event as string] = null
} else {
const fns = this._events[event as string]
if (fns) {
for (let i = 0; i < fns.length; i++) {
const f = fns[i]
if (fn === f || fn === (f as any).fn) {
fns.splice(i, 1)
break
}
}
}
}
}
return this
}
$emit(this: MountedComponent, name: string, ...payload: any[]) {
const parentListener =
this.$props['on' + name] || this.$props['on' + name.toLowerCase()]
if (parentListener) {
invokeListeners(parentListener, payload)
}
if (this._events) {
const handlers = this._events[name]
if (handlers) {
invokeListeners(handlers, payload)
}
}
return this
}
}
function invokeListeners(value: Function | Function[], payload: any[]) {
// TODO handle error
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
value[i](...payload)
}
} else {
value(...payload)
}
}

View File

@@ -0,0 +1,65 @@
import { EMPTY_OBJ } from './utils'
import { computed, ComputedGetter } from '@vue/observer'
import { Component, ComponentClass } from './component'
import { ComponentComputedOptions } from './componentOptions'
const extractionCache: WeakMap<
ComponentClass,
ComponentComputedOptions
> = new WeakMap()
export function getComputedOptions(
comp: ComponentClass
): ComponentComputedOptions {
let computedOptions = extractionCache.get(comp)
if (computedOptions) {
return computedOptions
}
computedOptions = {}
const descriptors = Object.getOwnPropertyDescriptors(comp.prototype as any)
for (const key in descriptors) {
const d = descriptors[key]
if (d.get) {
computedOptions[key] = d.get
// there's no need to do anything for the setter
// as it's already defined on the prototype
}
}
return computedOptions
}
export function initializeComputed(
instance: Component,
computedOptions: ComponentComputedOptions | undefined
) {
if (!computedOptions) {
instance.$computed = EMPTY_OBJ
return
}
const handles: Record<
string,
ComputedGetter
> = (instance._computedGetters = {})
const proxy = instance.$proxy
for (const key in computedOptions) {
handles[key] = computed(computedOptions[key], proxy)
}
instance.$computed = new Proxy(
{},
{
get(_, key: any) {
return handles[key]()
}
// TODO should be readonly
}
)
}
export function teardownComputed(instance: Component) {
const handles = instance._computedGetters
if (handles !== null) {
for (const key in handles) {
handles[key].stop()
}
}
}

View File

@@ -0,0 +1,51 @@
import { Slots } from './vdom'
import { MountedComponent } from './component'
export type Data = Record<string, any>
export interface RenderFunction<P = Data> {
(props: P, slots: Slots): any
}
export interface ComponentOptions<D = Data, P = Data> {
data?: () => Partial<D>
props?: ComponentPropsOptions<P>
computed?: ComponentComputedOptions<D, P>
watch?: ComponentWatchOptions<D, P>
render?: RenderFunction<P>
// TODO other options
readonly [key: string]: any
}
export type ComponentPropsOptions<P = Data> = {
[K in keyof P]: PropValidator<P[K]>
}
export type NormalizedPropsOptions<P = Data> = {
[K in keyof P]: PropOptions<P[K]>
}
export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
export type PropType<T> = Prop<T> | Prop<T>[]
export type PropValidator<T> = PropOptions<T> | PropType<T>
export interface PropOptions<T = any> {
type?: PropType<T>
required?: boolean
default?: T | null | undefined | (() => T | null | undefined)
validator?(value: T): boolean
}
export interface ComponentComputedOptions<D = Data, P = Data> {
[key: string]: (this: MountedComponent<D, P> & D & P, c: any) => any
}
export interface ComponentWatchOptions<D = Data, P = Data> {
[key: string]: (
this: MountedComponent<D, P> & D & P,
oldValue: any,
newValue: any
) => void
}

View File

@@ -0,0 +1,103 @@
import { EMPTY_OBJ, isReservedProp } from './utils'
import { Component, ComponentClass, MountedComponent } from './component'
import { immutable, unwrap, lock, unlock } from '@vue/observer'
import {
Data,
ComponentPropsOptions,
NormalizedPropsOptions,
PropValidator,
PropOptions
} from './componentOptions'
export function initializeProps(instance: Component, props: Data | null) {
instance.$props = immutable(props || {})
}
export function updateProps(instance: MountedComponent, nextProps: Data) {
// instance.$props is an observable that should not be replaced.
// instead, we mutate it to match latest props, which will trigger updates
// if any value has changed.
if (nextProps != null) {
const props = instance.$props
const rawProps = unwrap(props)
// unlock to temporarily allow mutatiing props
unlock()
for (const key in rawProps) {
if (!nextProps.hasOwnProperty(key)) {
delete props[key]
}
}
for (const key in nextProps) {
props[key] = nextProps[key]
}
lock()
}
}
// This is called for every component vnode created. This also means the data
// on every component vnode is guarunteed to be a fresh object.
export function normalizeComponentProps(
raw: any,
options: ComponentPropsOptions,
Component: ComponentClass
): Data {
if (!raw) {
return EMPTY_OBJ
}
const res: Data = {}
const normalizedOptions = options && normalizePropsOptions(options)
for (const key in raw) {
if (isReservedProp(key)) {
continue
}
if (__DEV__ && normalizedOptions != null) {
validateProp(key, raw[key], normalizedOptions[key], Component)
} else {
res[key] = raw[key]
}
}
// set default values
if (normalizedOptions != null) {
for (const key in normalizedOptions) {
if (res[key] === void 0) {
const opt = normalizedOptions[key]
if (opt != null && opt.hasOwnProperty('default')) {
const defaultValue = opt.default
res[key] =
typeof defaultValue === 'function' ? defaultValue() : defaultValue
}
}
}
}
return res
}
const normalizeCache: WeakMap<
ComponentPropsOptions,
NormalizedPropsOptions
> = new WeakMap()
function normalizePropsOptions(
raw: ComponentPropsOptions
): NormalizedPropsOptions {
let cached = normalizeCache.get(raw)
if (cached) {
return cached
}
const normalized: NormalizedPropsOptions = {}
for (const key in raw) {
const opt = raw[key]
normalized[key] =
typeof opt === 'function' ? { type: opt } : (opt as PropOptions)
}
normalizeCache.set(raw, normalized)
return normalized
}
function validateProp(
key: string,
value: any,
validator: PropValidator<any>,
Component: ComponentClass
) {
// TODO
}

View File

@@ -0,0 +1,82 @@
import { Component, MountedComponent } from './component'
const bindCache = new WeakMap()
function getBoundMethod(fn: Function, target: any, receiver: any): Function {
let boundMethodsForTarget = bindCache.get(target)
if (boundMethodsForTarget === void 0) {
bindCache.set(target, (boundMethodsForTarget = new Map()))
}
let boundFn = boundMethodsForTarget.get(fn)
if (boundFn === void 0) {
boundMethodsForTarget.set(fn, (boundFn = fn.bind(receiver)))
}
return boundFn
}
const renderProxyHandlers = {
get(target: MountedComponent, key: string, receiver: any) {
if (key === '_self') {
return target
} else if (
target._rawData !== null &&
target._rawData.hasOwnProperty(key)
) {
// data
return target.$data[key]
} else if (
target.$options.props != null &&
target.$options.props.hasOwnProperty(key)
) {
// props are only proxied if declared
return target.$props[key]
} else if (
target._computedGetters !== null &&
target._computedGetters.hasOwnProperty(key)
) {
// computed
return target._computedGetters[key]()
} else {
if (__DEV__ && !(key in target)) {
// TODO warn non-present property
}
const value = Reflect.get(target, key, receiver)
if (typeof value === 'function') {
// auto bind
return getBoundMethod(value, target, receiver)
} else {
return value
}
}
},
set(
target: MountedComponent,
key: string,
value: any,
receiver: any
): boolean {
if (__DEV__) {
if (typeof key === 'string' && key[0] === '$') {
// TODO warn setting immutable properties
return false
}
if (
target.$options.props != null &&
target.$options.props.hasOwnProperty(key)
) {
// TODO warn props are immutable
return false
}
}
if (target._rawData !== null && target._rawData.hasOwnProperty(key)) {
target.$data[key] = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
export function createRenderProxy(instance: Component): MountedComponent {
return new Proxy(instance, renderProxyHandlers) as MountedComponent
}

View File

@@ -0,0 +1,12 @@
import { EMPTY_OBJ } from './utils'
import { MountedComponent } from './component'
import { observable } from '@vue/observer'
export function initializeState(instance: MountedComponent) {
if (instance.data) {
instance._rawData = instance.data()
instance.$data = observable(instance._rawData)
} else {
instance.$data = EMPTY_OBJ
}
}

View File

@@ -0,0 +1,180 @@
import { VNodeFlags } from './flags'
import { EMPTY_OBJ } from './utils'
import { VNode, createFragment } from './vdom'
import { Component, MountedComponent, ComponentClass } from './component'
import { createTextVNode, cloneVNode } from './vdom'
import { initializeState } from './componentState'
import { initializeProps } from './componentProps'
import {
initializeComputed,
getComputedOptions,
teardownComputed
} from './componentComputed'
import { initializeWatch, teardownWatch } from './componentWatch'
import { Data, ComponentOptions } from './componentOptions'
import { createRenderProxy } from './componentProxy'
export function createComponentInstance(
vnode: VNode,
Component: ComponentClass,
parentComponent: MountedComponent | null
): MountedComponent {
const instance = (vnode.children = new Component()) as MountedComponent
instance.$parentVNode = vnode
// renderProxy
const proxy = (instance.$proxy = createRenderProxy(instance))
// pointer management
if (parentComponent) {
instance.$parent = parentComponent.$proxy
instance.$root = parentComponent.$root
parentComponent.$children.push(proxy)
} else {
instance.$root = proxy
}
// lifecycle
if (instance.beforeCreate) {
instance.beforeCreate.call(proxy)
}
// TODO provide/inject
initializeProps(instance, vnode.data)
initializeState(instance)
initializeComputed(instance, getComputedOptions(Component))
initializeWatch(instance, instance.$options.watch)
instance.$slots = vnode.slots || EMPTY_OBJ
if (instance.created) {
instance.created.call(proxy)
}
return instance as MountedComponent
}
export function renderInstanceRoot(instance: MountedComponent) {
// TODO handle render error
return normalizeComponentRoot(
instance.render.call(instance.$proxy, instance.$props, instance.$slots),
instance.$parentVNode
)
}
export function teardownComponentInstance(instance: MountedComponent) {
const parentComponent = instance.$parent && instance.$parent._self
if (parentComponent && !parentComponent._destroyed) {
parentComponent.$children.splice(
parentComponent.$children.indexOf(instance.$proxy),
1
)
}
teardownComputed(instance)
teardownWatch(instance)
}
export function normalizeComponentRoot(
vnode: any,
componentVNode: VNode | null
): VNode {
if (vnode == null) {
vnode = createTextVNode('')
} else if (typeof vnode !== 'object') {
vnode = createTextVNode(vnode + '')
} else if (Array.isArray(vnode)) {
vnode = createFragment(vnode)
} else {
const { flags } = vnode
// parentVNode data merge down
if (
componentVNode &&
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
) {
const parentData = componentVNode.data || EMPTY_OBJ
const childData = vnode.data || EMPTY_OBJ
let extraData: any = null
for (const key in parentData) {
// class/style bindings on parentVNode are merged down to child
// component root.
if (key === 'class') {
;(extraData || (extraData = {})).class = childData.class
? [].concat(childData.class, parentData.class)
: parentData.class
} else if (key === 'style') {
;(extraData || (extraData = {})).style = childData.style
? [].concat(childData.style, parentData.style)
: parentData.style
} else if (key.startsWith('nativeOn')) {
// nativeOn* handlers are merged down to child root as native listeners
const event = 'on' + key.slice(8)
;(extraData || (extraData = {}))[event] = childData.event
? [].concat(childData.event, parentData[key])
: parentData[key]
}
}
if (extraData) {
vnode = cloneVNode(vnode, extraData)
}
if (vnode.el) {
vnode = cloneVNode(vnode)
}
if (flags & VNodeFlags.COMPONENT) {
vnode.parentVNode = componentVNode
}
} else if (vnode.el) {
vnode = cloneVNode(vnode)
}
}
return vnode
}
export function shouldUpdateFunctionalComponent(
prevProps: Data | null,
nextProps: Data | null
): boolean {
if (prevProps === nextProps) {
return false
}
if (prevProps === null) {
return nextProps !== null
}
if (nextProps === null) {
return prevProps !== null
}
let shouldUpdate = true
const nextKeys = Object.keys(nextProps)
if (nextKeys.length === Object.keys(prevProps).length) {
shouldUpdate = false
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) {
shouldUpdate = true
}
}
}
return shouldUpdate
}
export function createComponentClassFromOptions(
options: ComponentOptions
): ComponentClass {
class ObjectComponent extends Component {
constructor() {
super()
this.$options = options
}
}
for (const key in options) {
const value = options[key]
if (typeof value === 'function') {
;(ObjectComponent.prototype as any)[key] = value
}
if (key === 'computed') {
const isGet = typeof value === 'function'
Object.defineProperty(ObjectComponent.prototype, key, {
configurable: true,
get: isGet ? value : value.get,
set: isGet ? undefined : value.set
})
}
}
return ObjectComponent as ComponentClass
}

View File

@@ -0,0 +1,50 @@
import { MountedComponent } from './component'
import { ComponentWatchOptions } from './componentOptions'
import { autorun, stop, Autorun } from '@vue/observer'
export function initializeWatch(
instance: MountedComponent,
options: ComponentWatchOptions | undefined
) {
if (options !== void 0) {
for (const key in options) {
setupWatcher(instance, key, options[key])
}
}
}
// TODO deep watch
export function setupWatcher(
instance: MountedComponent,
keyOrFn: string | Function,
cb: Function
): () => void {
const handles = instance._watchHandles || (instance._watchHandles = new Set())
const proxy = instance.$proxy
const rawGetter =
typeof keyOrFn === 'string'
? () => proxy[keyOrFn]
: () => keyOrFn.call(proxy)
let oldValue: any
const runner = autorun(rawGetter, {
scheduler: (runner: Autorun) => {
const newValue = runner()
if (newValue !== oldValue) {
cb(newValue, oldValue)
oldValue = newValue
}
}
})
oldValue = runner()
handles.add(runner)
return () => {
stop(runner)
handles.delete(runner)
}
}
export function teardownWatch(instance: MountedComponent) {
if (instance._watchHandles !== null) {
instance._watchHandles.forEach(stop)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
import { MountedComponent } from './component'
export const enum ErrorTypes {
LIFECYCLE = 1,
RENDER = 2,
NATIVE_EVENT_HANDLER = 3,
COMPONENT_EVENT_HANDLER = 4
}
const globalHandlers: Function[] = []
export function globalHandleError(handler: () => void) {
globalHandlers.push(handler)
return () => {
globalHandlers.splice(globalHandlers.indexOf(handler), 1)
}
}
export function handleError(
err: Error,
instance: MountedComponent,
type: ErrorTypes,
code: number
) {
// TODO
}

View File

@@ -0,0 +1,31 @@
// vnode flags
export const enum VNodeFlags {
ELEMENT_HTML = 1,
ELEMENT_SVG = 1 << 1,
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
COMPONENT_UNKNOWN = 1 << 2,
COMPONENT_STATEFUL = 1 << 3,
COMPONENT_FUNCTIONAL = 1 << 4,
COMPONENT_ASYNC = 1 << 5,
COMPONENT = COMPONENT_UNKNOWN |
COMPONENT_STATEFUL |
COMPONENT_FUNCTIONAL |
COMPONENT_ASYNC,
TEXT = 1 << 6,
FRAGMENT = 1 << 7,
PORTAL = 1 << 8
}
export const enum ChildrenFlags {
UNKNOWN_CHILDREN = 0,
NO_CHILDREN = 1,
SINGLE_VNODE = 1 << 1,
KEYED_VNODES = 1 << 2,
NONE_KEYED_VNODES = 1 << 3,
STABLE_SLOTS = 1 << 4,
DYNAMIC_SLOTS = 1 << 5,
HAS_SLOTS = STABLE_SLOTS | DYNAMIC_SLOTS,
MULTIPLE_VNODES = KEYED_VNODES | NONE_KEYED_VNODES
}

104
packages/core/src/h.ts Normal file
View File

@@ -0,0 +1,104 @@
import { ChildrenFlags } from './flags'
import { ComponentClass, FunctionalComponent } from './component'
import { ComponentOptions } from './componentOptions'
import {
VNode,
createElementVNode,
createComponentVNode,
createTextVNode,
createFragment,
createPortal
} from './vdom'
export const Fragment = Symbol()
export const Portal = Symbol()
type ElementType =
| string
| FunctionalComponent
| ComponentClass
| ComponentOptions
| typeof Fragment
| typeof Portal
export interface createElement {
(tag: ElementType, data: any, children: any): VNode
c: typeof createComponentVNode
e: typeof createElementVNode
t: typeof createTextVNode
f: typeof createFragment
p: typeof createPortal
}
export const h = ((tag: ElementType, data: any, children: any): VNode => {
if (Array.isArray(data) || (data !== void 0 && typeof data !== 'object')) {
children = data
data = null
}
// TODO clone data if it is observed
let key = null
let ref = null
let portalTarget = null
if (data != null) {
if (data.slots != null) {
children = data.slots
}
if (data.key != null) {
;({ key } = data)
}
if (data.ref != null) {
;({ ref } = data)
}
if (data.target != null) {
portalTarget = data.target
}
}
if (typeof tag === 'string') {
// element
return createElementVNode(
tag,
data,
children,
ChildrenFlags.UNKNOWN_CHILDREN,
key,
ref
)
} else if (tag === Fragment) {
if (__DEV__ && ref) {
// TODO warn fragment cannot have ref
}
return createFragment(children, ChildrenFlags.UNKNOWN_CHILDREN, key)
} else if (tag === Portal) {
if (__DEV__ && !portalTarget) {
// TODO warn portal must have a target
}
return createPortal(
portalTarget,
children,
ChildrenFlags.UNKNOWN_CHILDREN,
key,
ref
)
} else {
// TODO: handle fragment & portal types
// TODO: warn ref on fragment
// component
return createComponentVNode(
tag,
data,
children,
ChildrenFlags.UNKNOWN_CHILDREN,
key,
ref
)
}
}) as createElement
h.c = createComponentVNode
h.e = createElementVNode
h.t = createTextVNode
h.f = createFragment
h.p = createPortal

View File

@@ -0,0 +1,29 @@
// render api
export { h, Fragment, Portal } from './h'
export { cloneVNode, createPortal, createFragment } from './vdom'
export { createRenderer } from './createRenderer'
import { Component as InternalComponent, ComponentClass } from './component'
// the public component constructor with proper type inference.
export const Component = InternalComponent as ComponentClass
// observer api
export {
autorun,
stop,
observable,
immutable,
computed,
isObservable,
isImmutable,
markImmutable,
markNonReactive,
unwrap
} from '@vue/observer'
// flags & types
export { FunctionalComponent } from './component'
export { ComponentOptions, PropType } from './componentOptions'
export { VNodeFlags, ChildrenFlags } from './flags'
export { VNode, VNodeData, VNodeChildren, Key, Ref, Slots, Slot } from './vdom'

View File

@@ -0,0 +1,12 @@
export const EMPTY_OBJ: { readonly [key: string]: any } = Object.freeze({})
export const isReservedProp = (key: string): boolean => {
switch (key) {
case 'key':
case 'ref':
case 'slots':
return true
default:
return key.startsWith('nativeOn')
}
}

360
packages/core/src/vdom.ts Normal file
View File

@@ -0,0 +1,360 @@
import {
MountedComponent,
ComponentClass,
FunctionalComponent
} from './component'
import { VNodeFlags, ChildrenFlags } from './flags'
import { normalizeComponentProps } from './componentProps'
import { createComponentClassFromOptions } from './componentUtils'
import { ComponentPropsOptions } from './componentOptions'
// Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
export interface RenderNode {
vnode?: VNode | null
// technically this doesn't exist on platforn render nodes,
// but we list it here so that TS can figure out union types
$f: false
}
export interface RenderFragment {
children: (RenderNode | RenderFragment)[]
$f: true
}
export interface VNode {
_isVNode: true
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | RenderNode | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
key: Key | null
ref: Ref | null
slots: Slots | null
// only on mounted nodes
el: RenderNode | RenderFragment | null
// only on mounted component root nodes
// points to component node in parent tree
parentVNode: VNode | null
}
export interface MountedVNode extends VNode {
el: RenderNode | RenderFragment
}
export type MountedVNodes = MountedVNode[]
export interface VNodeData {
key?: Key | null
ref?: Ref | null
slots?: Slots | null
[key: string]: any
}
export type VNodeChildren =
| VNode[] // ELEMENT | PORTAL
| MountedComponent // COMPONENT_STATEFUL
| VNode // COMPONENT_FUNCTIONAL
| string // TEXT
| null
export type Key = string | number
export type Ref = (t: RenderNode | MountedComponent | null) => void
export interface Slots {
[name: string]: Slot
}
export type Slot = (...args: any[]) => VNode[]
export function createVNode(
flags: VNodeFlags,
tag: string | FunctionalComponent | ComponentClass | RenderNode | null,
data: VNodeData | null,
children: VNodeChildren | null,
childFlags: ChildrenFlags,
key: Key | null | undefined,
ref: Ref | null | undefined,
slots: Slots | null | undefined
): VNode {
const vnode: VNode = {
_isVNode: true,
flags,
tag,
data,
children,
childFlags,
key: key === void 0 ? null : key,
ref: ref === void 0 ? null : ref,
slots: slots === void 0 ? null : slots,
el: null,
parentVNode: null
}
if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
normalizeChildren(vnode, children)
}
return vnode
}
export function createElementVNode(
tag: string,
data: VNodeData | null,
children: VNodeChildren,
childFlags: ChildrenFlags,
key?: Key | null,
ref?: Ref | null
) {
const flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
return createVNode(flags, tag, data, children, childFlags, key, ref, null)
}
export function createComponentVNode(
comp: any,
data: VNodeData | null,
children: VNodeChildren,
childFlags: ChildrenFlags,
key?: Key | null,
ref?: Ref | null
) {
// resolve type
let flags: VNodeFlags
let propsOptions: ComponentPropsOptions
// flags
const compType = typeof comp
if (__COMPAT__ && compType === 'object') {
if (comp.functional) {
// object literal functional
flags = VNodeFlags.COMPONENT_FUNCTIONAL
const { render } = comp
if (!comp._normalized) {
render.pure = comp.pure
render.props = comp.props
comp._normalized = true
}
comp = render
propsOptions = comp.props
} else {
// object literal stateful
flags = VNodeFlags.COMPONENT_STATEFUL
comp =
comp._normalized ||
(comp._normalized = createComponentClassFromOptions(comp))
propsOptions = comp.options && comp.options.props
}
} else {
// assumes comp is function here now
if (__DEV__ && compType !== 'function') {
// TODO warn invalid comp value in dev
}
if (comp.prototype && comp.prototype.render) {
flags = VNodeFlags.COMPONENT_STATEFUL
propsOptions = comp.options && comp.options.props
} else {
flags = VNodeFlags.COMPONENT_FUNCTIONAL
propsOptions = comp.props
}
}
if (__DEV__ && flags === VNodeFlags.COMPONENT_FUNCTIONAL && ref) {
// TODO warn functional component cannot have ref
}
// props
const props = normalizeComponentProps(data, propsOptions, comp)
// slots
let slots: any
if (childFlags == null) {
childFlags = children
? ChildrenFlags.DYNAMIC_SLOTS
: ChildrenFlags.NO_CHILDREN
if (children != null) {
const childrenType = typeof children
if (childrenType === 'function') {
// function as children
slots = { default: children }
} else if (childrenType === 'object' && !(children as VNode)._isVNode) {
// slot object as children
slots = children
} else {
slots = { default: () => children }
}
slots = normalizeSlots(slots)
}
}
return createVNode(
flags,
comp,
props,
null, // to be set during mount
childFlags,
key,
ref,
slots
)
}
export function createTextVNode(text: string): VNode {
return createVNode(
VNodeFlags.TEXT,
null,
null,
text == null ? '' : text,
ChildrenFlags.NO_CHILDREN,
null,
null,
null
)
}
export function createFragment(
children: VNodeChildren,
childFlags?: ChildrenFlags,
key?: Key | null
) {
return createVNode(
VNodeFlags.FRAGMENT,
null,
null,
children,
childFlags === void 0 ? ChildrenFlags.UNKNOWN_CHILDREN : childFlags,
key,
null,
null
)
}
export function createPortal(
target: RenderNode | string,
children: VNodeChildren,
childFlags?: ChildrenFlags,
key?: Key | null,
ref?: Ref | null
): VNode {
return createVNode(
VNodeFlags.PORTAL,
target,
null,
children,
childFlags === void 0 ? ChildrenFlags.UNKNOWN_CHILDREN : childFlags,
key,
ref,
null
)
}
export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode {
const { flags, data } = vnode
if (flags & VNodeFlags.ELEMENT || flags & VNodeFlags.COMPONENT) {
let clonedData = data
if (extraData != null) {
clonedData = {}
if (data != null) {
for (const key in data) {
clonedData[key] = data[key]
}
}
for (const key in extraData) {
clonedData[key] = extraData[key]
}
}
return createVNode(
flags,
vnode.tag,
clonedData,
vnode.children,
vnode.childFlags,
vnode.key,
vnode.ref,
vnode.slots
)
} else if (flags & VNodeFlags.TEXT) {
return createTextVNode(vnode.children as string)
} else {
return vnode
}
}
function normalizeChildren(vnode: VNode, children: any) {
let childFlags
if (Array.isArray(children)) {
const { length } = children
if (length === 0) {
childFlags = ChildrenFlags.NO_CHILDREN
children = null
} else if (length === 1) {
childFlags = ChildrenFlags.SINGLE_VNODE
children = children[0]
if (children.el) {
children = cloneVNode(children)
}
} else {
childFlags = ChildrenFlags.KEYED_VNODES
children = normalizeVNodes(children)
}
} else if (children == null) {
childFlags = ChildrenFlags.NO_CHILDREN
} else if (children._isVNode) {
childFlags = ChildrenFlags.SINGLE_VNODE
if (children.el) {
children = cloneVNode(children)
}
} else {
// primitives or invalid values, cast to string
childFlags = ChildrenFlags.SINGLE_VNODE
children = createTextVNode(children + '')
}
vnode.children = children
vnode.childFlags = childFlags
}
export function normalizeVNodes(
children: any[],
newChildren: VNode[] = [],
currentPrefix: string = ''
): VNode[] {
for (let i = 0; i < children.length; i++) {
const child = children[i]
let newChild
if (child == null) {
newChild = createTextVNode('')
} else if (child._isVNode) {
newChild = child.el ? cloneVNode(child) : child
} else if (Array.isArray(child)) {
normalizeVNodes(child, newChildren, currentPrefix + i + '|')
} else {
newChild = createTextVNode(child + '')
}
if (newChild) {
if (newChild.key == null) {
newChild.key = currentPrefix + i
}
newChildren.push(newChild)
}
}
return newChildren
}
// ensure all slot functions return Arrays
function normalizeSlots(slots: { [name: string]: any }): Slots {
const normalized: Slots = {}
for (const name in slots) {
normalized[name] = (...args) => normalizeSlot(slots[name](...args))
}
return normalized
}
function normalizeSlot(value: any): VNode[] {
if (value == null) {
return [createTextVNode('')]
} else if (Array.isArray(value)) {
return normalizeVNodes(value)
} else if (value._isVNode) {
return [value]
} else {
return [createTextVNode(value + '')]
}
}