import { isArray, isFunction, isString, isObject, EMPTY_ARR, extend, normalizeClass, normalizeStyle, PatchFlags, ShapeFlags } from '@vue/shared' import { ComponentInternalInstance, Data, Component, ClassComponent } from './component' import { RawSlots } from './componentSlots' import { isProxy, Ref, toRaw } from '@vue/reactivity' import { AppContext } from './apiCreateApp' import { SuspenseImpl, isSuspense, SuspenseBoundary } from './components/Suspense' import { DirectiveBinding } from './directives' import { TransitionHooks } from './components/BaseTransition' import { warn } from './warning' import { currentScopeId } from './helpers/scopeId' import { TeleportImpl, isTeleport } from './components/Teleport' import { currentRenderingInstance } from './componentRenderUtils' import { RendererNode, RendererElement } from './renderer' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true new (): { $props: VNodeProps } } export const Text = Symbol(__DEV__ ? 'Text' : undefined) export const Comment = Symbol(__DEV__ ? 'Comment' : undefined) export const Static = Symbol(__DEV__ ? 'Static' : undefined) export type VNodeTypes = | string | Component | typeof Text | typeof Static | typeof Comment | typeof Fragment | typeof TeleportImpl | typeof SuspenseImpl export type VNodeRef = | string | Ref | ((ref: object | null, refs: Record) => void) export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef] type VNodeMountHook = (vnode: VNode) => void type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void export type VNodeHook = | VNodeMountHook | VNodeUpdateHook | VNodeMountHook[] | VNodeUpdateHook[] export interface VNodeProps { [key: string]: any key?: string | number ref?: VNodeRef // vnode hooks onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[] onVnodeMounted?: VNodeMountHook | VNodeMountHook[] onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[] onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[] onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[] onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[] } type VNodeChildAtom = | VNode | string | number | boolean | null | undefined | void export interface VNodeArrayChildren extends Array {} export type VNodeChild = VNodeChildAtom | VNodeArrayChildren export type VNodeNormalizedChildren = | string | VNodeArrayChildren | RawSlots | null export interface VNode { /** * @internal */ __v_isVNode: true /** * @internal */ __v_skip: true type: VNodeTypes props: VNodeProps | null key: string | number | null ref: VNodeNormalizedRef | null scopeId: string | null // SFC only children: VNodeNormalizedChildren component: ComponentInternalInstance | null suspense: SuspenseBoundary | null dirs: DirectiveBinding[] | null transition: TransitionHooks | null // DOM el: HostNode | null anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target targetAnchor: HostNode | null // teleport target anchor // optimization only shapeFlag: number patchFlag: number dynamicProps: string[] | null dynamicChildren: VNode[] | null // application root node only appContext: AppContext | null } // Since v-if and v-for are the two possible ways node structure can dynamically // change, once we consider v-if branches and each v-for fragment a block, we // can divide a template into nested blocks, and within each block the node // structure would be stable. This allows us to skip most children diffing // and only worry about the dynamic nodes (indicated by patch flags). const blockStack: (VNode[] | null)[] = [] let currentBlock: VNode[] | null = null /** * Open a block. * This must be called before `createBlock`. It cannot be part of `createBlock` * because the children of the block are evaluated before `createBlock` itself * is called. The generated code typically looks like this: * * ```js * function render() { * return (openBlock(),createBlock('div', null, [...])) * } * ``` * disableTracking is true when creating a fragment block, since a fragment * always diffs its children. * * @internal */ export function openBlock(disableTracking = false) { blockStack.push((currentBlock = disableTracking ? null : [])) } // Whether we should be tracking dynamic child nodes inside a block. // Only tracks when this value is > 0 // We are not using a simple boolean because this value may need to be // incremented/decremented by nested usage of v-once (see below) let shouldTrack = 1 /** * Block tracking sometimes needs to be disabled, for example during the * creation of a tree that needs to be cached by v-once. The compiler generates * code like this: * * ``` js * _cache[1] || ( * setBlockTracking(-1), * _cache[1] = createVNode(...), * setBlockTracking(1), * _cache[1] * ) * ``` * * @internal */ export function setBlockTracking(value: number) { shouldTrack += value } /** * Create a block root vnode. Takes the same exact arguments as `createVNode`. * A block root keeps track of dynamic nodes within the block in the * `dynamicChildren` array. * * @internal */ export function createBlock( type: VNodeTypes | ClassComponent, props?: { [key: string]: any } | null, children?: any, patchFlag?: number, dynamicProps?: string[] ): VNode { const vnode = createVNode( type, props, children, patchFlag, dynamicProps, true /* isBlock: prevent a block from tracking itself */ ) // save current block children on the block vnode vnode.dynamicChildren = currentBlock || EMPTY_ARR // close block blockStack.pop() currentBlock = blockStack[blockStack.length - 1] || null // a block is always going to be patched, so track it as a child of its // parent block if (currentBlock) { currentBlock.push(vnode) } return vnode } export function isVNode(value: any): value is VNode { return value ? value.__v_isVNode === true : false } export function isSameVNodeType(n1: VNode, n2: VNode): boolean { if ( __DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && (n2.type as Component).__hmrUpdated ) { // HMR only: if the component has been hot-updated, force a reload. return false } return n1.type === n2.type && n1.key === n2.key } let vnodeArgsTransformer: | (( args: Parameters, instance: ComponentInternalInstance | null ) => Parameters) | undefined /** * Internal API for registering an arguments transform for createVNode * used for creating stubs in the test-utils * It is *internal* but needs to be exposed for test-utils to pick up proper * typings */ export function transformVNodeArgs(transformer?: typeof vnodeArgsTransformer) { vnodeArgsTransformer = transformer } const createVNodeWithArgsTransform = ( ...args: Parameters ): VNode => { return _createVNode( ...(vnodeArgsTransformer ? vnodeArgsTransformer(args, currentRenderingInstance) : args) ) } export const InternalObjectKey = `__vInternal` const normalizeKey = ({ key }: VNodeProps): VNode['key'] => key != null ? key : null const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => (ref != null ? isArray(ref) ? ref : [currentRenderingInstance!, ref] : null) as any export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode { if (!type || type === NULL_DYNAMIC_COMPONENT) { if (__DEV__ && !type) { warn(`Invalid vnode type when creating vnode: ${type}.`) } type = Comment } // class component normalization. if (isFunction(type) && '__vccOpts' in type) { type = type.__vccOpts } // class & style normalization. if (props) { // for reactive or proxy objects, we need to clone it to enable mutation. if (isProxy(props) || InternalObjectKey in props) { props = extend({}, props) } let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { // reactive state objects need to be cloned since they are likely to be // mutated if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } } // encode the vnode type information into a bitmap const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) { type = toRaw(type) warn( `Vue received a Component which was made a reactive object. This can ` + `lead to unnecessary performance overhead, and should be avoided by ` + `marking the component with \`markRaw\` or using \`shallowRef\` ` + `instead of \`ref\`.`, `\nComponent that was made reactive: `, type ) } const vnode: VNode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, children: null, component: null, suspense: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null } normalizeChildren(vnode, children) // presence of a patch flag indicates this node needs patching on updates. // component nodes also should always be patched, because even if the // component doesn't need to update, it needs to persist the instance on to // the next vnode so that it can be properly unmounted later. if ( shouldTrack > 0 && !isBlockNode && currentBlock && // the EVENTS flag is only for hydration and if it is the only flag, the // vnode should not be considered dynamic due to handler caching. patchFlag !== PatchFlags.HYDRATE_EVENTS && (patchFlag > 0 || shapeFlag & ShapeFlags.SUSPENSE || shapeFlag & ShapeFlags.TELEPORT || shapeFlag & ShapeFlags.STATEFUL_COMPONENT || shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) ) { currentBlock.push(vnode) } return vnode } export function cloneVNode( vnode: VNode, extraProps?: Data & VNodeProps ): VNode { const props = (extraProps ? vnode.props ? mergeProps(vnode.props, extraProps) : extend({}, extraProps) : vnode.props) as any // This is intentionally NOT using spread or extend to avoid the runtime // key enumeration cost. return { __v_isVNode: true, __v_skip: true, type: vnode.type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: vnode.scopeId, children: vnode.children, target: vnode.target, targetAnchor: vnode.targetAnchor, shapeFlag: vnode.shapeFlag, // if the vnode is cloned with extra props, we can no longer assume its // existing patch flag to be reliable and need to bail out of optimized mode. // however we don't want block nodes to de-opt their children, so if the // vnode is a block node, we only add the FULL_PROPS flag to it. patchFlag: extraProps ? vnode.dynamicChildren ? vnode.patchFlag | PatchFlags.FULL_PROPS : PatchFlags.BAIL : vnode.patchFlag, dynamicProps: vnode.dynamicProps, dynamicChildren: vnode.dynamicChildren, appContext: vnode.appContext, dirs: vnode.dirs, transition: vnode.transition, // These should technically only be non-null on mounted VNodes. However, // they *should* be copied for kept-alive vnodes. So we just always copy // them since them being non-null during a mount doesn't affect the logic as // they will simply be overwritten. component: vnode.component, suspense: vnode.suspense, el: vnode.el, anchor: vnode.anchor } } /** * @internal */ export function createTextVNode(text: string = ' ', flag: number = 0): VNode { return createVNode(Text, null, text, flag) } /** * @internal */ export function createStaticVNode(content: string): VNode { return createVNode(Static, null, content) } /** * @internal */ export function createCommentVNode( text: string = '', // when used as the v-else branch, the comment node must be created as a // block to ensure correct updates. asBlock: boolean = false ): VNode { return asBlock ? (openBlock(), createBlock(Comment, null, text)) : createVNode(Comment, null, text) } export function normalizeVNode(child: VNodeChild): VNode { if (child == null || typeof child === 'boolean') { // empty placeholder return createVNode(Comment) } else if (isArray(child)) { // fragment return createVNode(Fragment, null, child) } else if (typeof child === 'object') { // already vnode, this should be the most common since compiled templates // always produce all-vnode children arrays return child.el === null ? child : cloneVNode(child) } else { // strings and numbers return createVNode(Text, null, String(child)) } } // optimized normalization for template-compiled render fns export function cloneIfMounted(child: VNode): VNode { return child.el === null ? child : cloneVNode(child) } export function normalizeChildren(vnode: VNode, children: unknown) { let type = 0 const { shapeFlag } = vnode if (children == null) { children = null } else if (isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'object') { // Normalize slot to plain children if ( (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) && (children as any).default ) { normalizeChildren(vnode, (children as any).default()) return } else { type = ShapeFlags.SLOTS_CHILDREN if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) { // if slots are not normalized, attach context instance // (compiled / normalized slots already have context) ;(children as RawSlots)._ctx = currentRenderingInstance } } } else if (isFunction(children)) { children = { default: children, _ctx: currentRenderingInstance } type = ShapeFlags.SLOTS_CHILDREN } else { children = String(children) // force teleport children to array so it can be moved around if (shapeFlag & ShapeFlags.TELEPORT) { type = ShapeFlags.ARRAY_CHILDREN children = [createTextVNode(children as string)] } else { type = ShapeFlags.TEXT_CHILDREN } } vnode.children = children as VNodeNormalizedChildren vnode.shapeFlag |= type } const handlersRE = /^on|^vnode/ export function mergeProps(...args: (Data & VNodeProps)[]) { const ret: Data = {} extend(ret, args[0]) for (let i = 1; i < args.length; i++) { const toMerge = args[i] for (const key in toMerge) { if (key === 'class') { if (ret.class !== toMerge.class) { ret.class = normalizeClass([ret.class, toMerge.class]) } } else if (key === 'style') { ret.style = normalizeStyle([ret.style, toMerge.style]) } else if (handlersRE.test(key)) { // on*, vnode* const existing = ret[key] const incoming = toMerge[key] if (existing !== incoming) { ret[key] = existing ? [].concat(existing as any, toMerge[key] as any) : incoming } } else { ret[key] = toMerge[key] } } } return ret }