import { Text, Fragment, Comment, Portal, cloneIfMounted, normalizeVNode, VNode, VNodeArrayChildren, createVNode, isSameVNodeType, Static } from './vnode' import { ComponentInternalInstance, createComponentInstance, Component, Data, setupComponent } from './component' import { renderComponentRoot, shouldUpdateComponent, updateHOCHostEl } from './componentRenderUtils' import { isString, EMPTY_OBJ, EMPTY_ARR, isReservedProp, isFunction, PatchFlags, NOOP } from '@vue/shared' import { queueJob, queuePostFlushCb, flushPostFlushCbs, invalidateJob } from './scheduler' import { effect, stop, ReactiveEffectOptions, isRef, Ref, toRaw, DebuggerEvent } from '@vue/reactivity' import { resolveProps } from './componentProps' import { resolveSlots } from './componentSlots' import { ShapeFlags } from './shapeFlags' import { pushWarningContext, popWarningContext, warn } from './warning' import { invokeDirectiveHook } from './directives' import { ComponentPublicInstance } from './componentProxy' import { createAppAPI } from './apiCreateApp' import { SuspenseBoundary, queueEffectWithSuspense, SuspenseImpl } from './components/Suspense' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' import { registerHMR, unregisterHMR } from './hmr' import { createHydrationFunctions } from './hydration' const __HMR__ = __BUNDLER__ && __DEV__ export interface RendererOptions { patchProp( el: HostElement, key: string, value: any, oldValue: any, isSVG?: boolean, prevChildren?: VNode[], parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, unmountChildren?: ( children: VNode[], parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null ) => void ): void insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void remove(el: HostNode): void createElement(type: string, isSVG?: boolean): HostElement createText(text: string): HostNode createComment(text: string): HostNode setText(node: HostNode, text: string): void setElementText(node: HostElement, text: string): void parentNode(node: HostNode): HostElement | null nextSibling(node: HostNode): HostNode | null querySelector?(selector: string): HostElement | null setScopeId?(el: HostElement, id: string): void cloneNode?(node: HostNode): HostNode insertStaticContent?( content: string, parent: HostElement, anchor: HostNode | null, isSVG: boolean ): HostElement } export type RootRenderFunction = ( vnode: VNode | null, dom: HostElement ) => void // An object exposing the internals of a renderer, passed to tree-shakeable // features so that they can be decoupled from this file. export interface RendererInternals { patch: ( n1: VNode | null, // null means this is a mount n2: VNode, container: HostElement, anchor?: HostNode | null, parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, isSVG?: boolean, optimized?: boolean ) => void unmount: ( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, doRemove?: boolean ) => void move: ( vnode: VNode, container: HostElement, anchor: HostNode | null, type: MoveType, parentSuspense?: SuspenseBoundary | null ) => void next: (vnode: VNode) => HostNode | null options: RendererOptions } export const enum MoveType { ENTER, LEAVE, REORDER } const prodEffectOptions = { scheduler: queueJob } function createDevEffectOptions( instance: ComponentInternalInstance ): ReactiveEffectOptions { return { scheduler: queueJob, onTrack: instance.rtc ? e => invokeHooks(instance.rtc!, e) : void 0, onTrigger: instance.rtg ? e => invokeHooks(instance.rtg!, e) : void 0 } } export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) { for (let i = 0; i < hooks.length; i++) { hooks[i](arg) } } export const queuePostRenderEffect = __FEATURE_SUSPENSE__ ? queueEffectWithSuspense : queuePostFlushCb /** * The createRenderer function accepts two generic arguments: * HostNode and HostElement, corresponding to Node and Element types in the * host environment. For example, for runtime-dom, HostNode would be the DOM * `Node` interface and HostElement would be the DOM `Element` interface. * * Custom renderers can pass in the platform specific types like this: * * ``` js * const { render, createApp } = createRenderer({ * patchProp, * ...nodeOps * }) * ``` */ export function createRenderer< HostNode extends object = any, HostElement extends HostNode = any >(options: RendererOptions) { const res = baseCreateRenderer(options) return res as typeof res & { hydrate: undefined } } // Separate API for creating hydration-enabled renderer. // Hydration logic is only used when calling this function, making it // tree-shakable. export function createHydrationRenderer< HostNode extends object = any, HostElement extends HostNode = any >(options: RendererOptions) { const res = baseCreateRenderer(options, createHydrationFunctions) return res as typeof res & { hydrate: ReturnType[0] } } function baseCreateRenderer< HostNode extends object = any, HostElement extends HostNode = any >( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ) { type HostVNode = VNode type HostVNodeChildren = VNodeArrayChildren type HostSuspenseBoundary = SuspenseBoundary const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options const internals: RendererInternals = { patch, unmount, move, next: getNextHostNode, options } let hydrate: ReturnType[0] | undefined let hydrateNode: ReturnType[1] | undefined if (createHydrationFns) { ;[hydrate, hydrateNode] = createHydrationFns(mountComponent, hostPatchProp) } function patch( n1: HostVNode | null, // null means this is a mount n2: HostVNode, container: HostElement, anchor: HostNode | null = null, parentComponent: ComponentInternalInstance | null = null, parentSuspense: HostSuspenseBoundary | null = null, isSVG: boolean = false, optimized: boolean = false ) { // patching & not same type, unmount old tree if (n1 != null && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } const { type, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } // static nodes are noop on patch break case Fragment: processFragment( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) break case Portal: processPortal( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals ) } else if (__DEV__) { warn('Invalid HostVNode type:', type, `(${typeof type})`) } } } function processText( n1: HostVNode | null, n2: HostVNode, container: HostElement, anchor: HostNode | null ) { if (n1 == null) { hostInsert( (n2.el = hostCreateText(n2.children as string)), container, anchor ) } else { const el = (n2.el = n1.el) as HostNode if (n2.children !== n1.children) { hostSetText(el, n2.children as string) } } } function processCommentNode( n1: HostVNode | null, n2: HostVNode, container: HostElement, anchor: HostNode | null ) { if (n1 == null) { hostInsert( (n2.el = hostCreateComment((n2.children as string) || '')), container, anchor ) } else { // there's no support for dynamic comments n2.el = n1.el } } function mountStaticNode( n2: HostVNode, container: HostElement, anchor: HostNode | null, isSVG: boolean ) { if (n2.el != null && hostCloneNode !== undefined) { hostInsert(hostCloneNode(n2.el), container, anchor) } else { // static nodes are only present when used with compiler-dom/runtime-dom // which guarantees presence of hostInsertStaticContent. n2.el = hostInsertStaticContent!( n2.children as string, container, anchor, isSVG ) } } function processElement( n1: HostVNode | null, n2: HostVNode, container: HostElement, anchor: HostNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean, optimized: boolean ) { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } if (n2.ref !== null && parentComponent !== null) { setRef(n2.ref, n1 && n1.ref, parentComponent, n2.el) } } function mountElement( vnode: HostVNode, container: HostElement, anchor: HostNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean, optimized: boolean ) { let el: HostElement const { type, props, shapeFlag, transition, scopeId, patchFlag } = vnode if ( vnode.el !== null && hostCloneNode !== undefined && patchFlag === PatchFlags.HOISTED ) { // If a vnode has non-null el, it means it's being reused. // Only static vnodes can be reused, so its mounted DOM nodes should be // exactly the same, and we can simply do a clone here. el = vnode.el = hostCloneNode(vnode.el) as HostElement } else { el = vnode.el = hostCreateElement(vnode.type as string, isSVG) // props if (props != null) { for (const key in props) { if (!isReservedProp(key)) { hostPatchProp(el, key, props[key], null, isSVG) } } if (props.onVnodeBeforeMount != null) { invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) } } // scopeId if (__BUNDLER__) { if (scopeId !== null) { hostSetScopeId(el, scopeId) } const treeOwnerId = parentComponent && parentComponent.type.__scopeId // vnode's own scopeId and the current patched component's scopeId is // different - this is a slot content node. if (treeOwnerId != null && treeOwnerId !== scopeId) { hostSetScopeId(el, treeOwnerId + '-s') } } // children if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children as string) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( vnode.children as HostVNodeChildren, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || vnode.dynamicChildren !== null ) } if (transition != null && !transition.persisted) { transition.beforeEnter(el) } } hostInsert(el, container, anchor) const vnodeMountedHook = props && props.onVnodeMounted if ( vnodeMountedHook != null || (transition != null && !transition.persisted) ) { queuePostRenderEffect(() => { vnodeMountedHook && invokeDirectiveHook(vnodeMountedHook, parentComponent, vnode) transition && !transition.persisted && transition.enter(el) }, parentSuspense) } } function mountChildren( children: HostVNodeChildren, container: HostElement, anchor: HostNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean, optimized: boolean, start: number = 0 ) { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i] as HostVNode) : normalizeVNode(children[i])) patch( null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } function patchElement( n1: HostVNode, n2: HostVNode, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean, optimized: boolean ) { const el = (n2.el = n1.el) as HostElement let { patchFlag, dynamicChildren } = n2 const oldProps = (n1 && n1.props) || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ if (newProps.onVnodeBeforeUpdate != null) { invokeDirectiveHook(newProps.onVnodeBeforeUpdate, parentComponent, n2, n1) } if (__HMR__ && parentComponent && parentComponent.renderUpdated) { // HMR updated, force full diff patchFlag = 0 optimized = false dynamicChildren = null } if (patchFlag > 0) { // the presence of a patchFlag means this element's render code was // generated by the compiler and can take the fast path. // in this path old node and new node are guaranteed to have the same shape // (i.e. at the exact same position in the source template) if (patchFlag & PatchFlags.FULL_PROPS) { // element props contain dynamic keys, full diff needed patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG ) } else { // class // this flag is matched when the element has dynamic class bindings. if (patchFlag & PatchFlags.CLASS) { if (oldProps.class !== newProps.class) { hostPatchProp(el, 'class', newProps.class, null, isSVG) } } // style // this flag is matched when the element has dynamic style bindings if (patchFlag & PatchFlags.STYLE) { hostPatchProp(el, 'style', newProps.style, oldProps.style, isSVG) } // props // This flag is matched when the element has dynamic prop/attr bindings // other than class and style. The keys of dynamic prop/attrs are saved for // faster iteration. // Note dynamic keys like :[foo]="bar" will cause this optimization to // bail out and go through a full diff because we need to unset the old key if (patchFlag & PatchFlags.PROPS) { // if the flag is present then dynamicProps must be non-null const propsToUpdate = n2.dynamicProps! for (let i = 0; i < propsToUpdate.length; i++) { const key = propsToUpdate[i] const prev = oldProps[key] const next = newProps[key] if (prev !== next) { hostPatchProp( el, key, next, prev, isSVG, n1.children as HostVNode[], parentComponent, parentSuspense, unmountChildren ) } } } } // text // This flag is matched when the element has only dynamic text children. if (patchFlag & PatchFlags.TEXT) { if (n1.children !== n2.children) { hostSetElementText(el, n2.children as string) } } } else if (!optimized && dynamicChildren == null) { // unoptimized, full diff patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG ) } const areChildrenSVG = isSVG && n2.type !== 'foreignObject' if (dynamicChildren != null) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) } else if (!optimized) { // full diff patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG ) } if (newProps.onVnodeUpdated != null) { queuePostRenderEffect(() => { invokeDirectiveHook(newProps.onVnodeUpdated, parentComponent, n2, n1) }, parentSuspense) } } // The fast path for blocks. function patchBlockChildren( oldChildren: HostVNode[], newChildren: HostVNode[], fallbackContainer: HostElement, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean ) { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] // Determine the container (parent element) for the patch. const container = // - In the case of a Fragment, we need to provide the actual parent // of the Fragment itself so it can move its children. oldVNode.type === Fragment || // - In the case of different nodes, there is going to be a replacement // which also requires the correct parent container !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. oldVNode.shapeFlag & ShapeFlags.COMPONENT ? hostParentNode(oldVNode.el!)! : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true ) } } function patchProps( el: HostElement, vnode: HostVNode, oldProps: Data, newProps: Data, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean ) { if (oldProps !== newProps) { for (const key in newProps) { if (isReservedProp(key)) continue const next = newProps[key] const prev = oldProps[key] if (next !== prev) { hostPatchProp( el, key, next, prev, isSVG, vnode.children as HostVNode[], parentComponent, parentSuspense, unmountChildren ) } } if (oldProps !== EMPTY_OBJ) { for (const key in oldProps) { if (!isReservedProp(key) && !(key in newProps)) { hostPatchProp( el, key, null, null, isSVG, vnode.children as HostVNode[], parentComponent, parentSuspense, unmountChildren ) } } } } } let devFragmentID = 0 function processFragment( n1: HostVNode | null, n2: HostVNode, container: HostElement, anchor: HostNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: HostSuspenseBoundary | null, isSVG: boolean, optimized: boolean ) { const showID = __DEV__ && !__TEST__ const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateComment(showID ? `fragment-${devFragmentID}-start` : ''))! const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateComment(showID ? `fragment-${devFragmentID}-end` : ''))! let { patchFlag, dynamicChildren } = n2 if (patchFlag > 0) { optimized = true } if (__HMR__ && parentComponent && parentComponent.renderUpdated) { // HMR updated, force full diff patchFlag = 0 optimized = false dynamicChildren = null } if (n1 == null) { if (showID) { devFragmentID++ } hostInsert(fragmentStartAnchor, container, anchor) hostInsert(fragmentEndAnchor, container, anchor) // a fragment can only have array children // since they are either generated by the compiler, or implicitly created // from arrays. mountChildren( n2.children as HostVNodeChildren, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, optimized ) } else { if (patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren != null) { // a stable fragment (template root or