382 lines
9.4 KiB
TypeScript
382 lines
9.4 KiB
TypeScript
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) {
|
|
const existing = clonedData[key]
|
|
const extra = extraData[key]
|
|
if (extra === void 0) {
|
|
continue
|
|
}
|
|
// special merge behavior for attrs / class / style / on.
|
|
let isOn
|
|
if (key === 'attrs') {
|
|
clonedData.attrs = existing
|
|
? Object.assign({}, existing, extra)
|
|
: extra
|
|
} else if (
|
|
key === 'class' ||
|
|
key === 'style' ||
|
|
(isOn = key.startsWith('on'))
|
|
) {
|
|
// all three props can handle array format, so we simply merge them
|
|
// by concating.
|
|
clonedData[key] = existing ? [].concat(existing, extra) : extra
|
|
} else {
|
|
clonedData[key] = extra
|
|
}
|
|
}
|
|
}
|
|
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 + '')]
|
|
}
|
|
}
|