vue3-yuanma/packages/core/src/vdom.ts

382 lines
9.4 KiB
TypeScript
Raw Normal View History

2018-09-19 23:35:38 +08:00
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) {
2018-09-24 09:54:48 +08:00
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
}
2018-09-19 23:35:38 +08:00
}
}
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 + '')]
}
}