import { ATTR_KEY } from '../constants'; import { isSameNodeType, isNamedNode } from './index'; import { buildComponentFromVNode } from './component'; import { createNode, setAccessor } from '../dom/index'; import { unmountComponent } from './component'; import options from '../options'; import { applyRef } from '../util'; import { removeNode } from '../dom/index'; /** * Queue of components that have been mounted and are awaiting componentDidMount * @type {Array} */ export const mounts = []; /** Diff recursion count, used to track the end of the diff cycle. */ export let diffLevel = 0; /** Global flag indicating if the diff is currently within an SVG */ let isSvgMode = false; /** Global flag indicating if the diff is performing hydration */ let hydrating = false; /** Invoke queued componentDidMount lifecycle methods */ export function flushMounts() { let c; while ((c = mounts.shift())) { if (options.afterMount) options.afterMount(c); if (c.componentDidMount) c.componentDidMount(); } } /** * Apply differences in a given vnode (and it's deep children) to a real DOM Node. * @param {import('../dom').PreactElement} dom A DOM node to mutate into the shape of a `vnode` * @param {import('../vnode').VNode} vnode A VNode (with descendants forming a tree) representing * the desired DOM structure * @param {object} context The current context * @param {boolean} mountAll Whether or not to immediately mount all components * @param {Element} parent ? * @param {boolean} componentRoot ? * @returns {import('../dom').PreactElement} The created/mutated element * @private */ export function diff(dom, vnode, context, mountAll, parent, componentRoot) { // diffLevel having been 0 here indicates initial entry into the diff (not a subdiff) if (!diffLevel++) { // when first starting the diff, check if we're diffing an SVG or within an SVG isSvgMode = parent!=null && parent.ownerSVGElement!==undefined; // hydration is indicated by the existing element to be diffed not having a prop cache hydrating = dom!=null && !(ATTR_KEY in dom); } let ret = idiff(dom, vnode, context, mountAll, componentRoot); // append the element if its a new parent if (parent && ret.parentNode!==parent) parent.appendChild(ret); // diffLevel being reduced to 0 means we're exiting the diff if (!--diffLevel) { hydrating = false; // invoke queued componentDidMount lifecycle methods if (!componentRoot) flushMounts(); } return ret; } /** * Internals of `diff()`, separated to allow bypassing diffLevel / mount flushing. * @param {import('../dom').PreactElement} dom A DOM node to mutate into the shape of a `vnode` * @param {import('../vnode').VNode} vnode A VNode (with descendants forming a tree) representing the desired DOM structure * @param {object} context The current context * @param {boolean} mountAll Whether or not to immediately mount all components * @param {boolean} [componentRoot] ? * @private */ function idiff(dom, vnode, context, mountAll, componentRoot) { let out = dom, prevSvgMode = isSvgMode; // empty values (null, undefined, booleans) render as empty Text nodes if (vnode==null || typeof vnode==='boolean') vnode = ''; // Fast case: Strings & Numbers create/update Text nodes. if (typeof vnode==='string' || typeof vnode==='number') { // update if it's already a Text node: if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) { /* istanbul ignore if */ /* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */ if (dom.nodeValue!=vnode) { dom.nodeValue = vnode; } } else { // it wasn't a Text node: replace it with one and recycle the old Element out = document.createTextNode(vnode); if (dom) { if (dom.parentNode) dom.parentNode.replaceChild(out, dom); recollectNodeTree(dom, true); } } out[ATTR_KEY] = true; return out; } // If the VNode represents a Component, perform a component diff: let vnodeName = vnode.nodeName; if (typeof vnodeName==='function') { return buildComponentFromVNode(dom, vnode, context, mountAll); } // Tracks entering and exiting SVG namespace when descending through the tree. isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode; // If there's no existing element or it's the wrong type, create a new one: vnodeName = String(vnodeName); if (!dom || !isNamedNode(dom, vnodeName)) { out = createNode(vnodeName, isSvgMode); if (dom) { // move children into the replacement node while (dom.firstChild) out.appendChild(dom.firstChild); // if the previous Element was mounted into the DOM, replace it inline if (dom.parentNode) dom.parentNode.replaceChild(out, dom); // recycle the old element (skips non-Element node types) recollectNodeTree(dom, true); } } let fc = out.firstChild, props = out[ATTR_KEY], vchildren = vnode.children; if (props==null) { props = out[ATTR_KEY] = {}; for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value; } // Optimization: fast-path for elements containing a single TextNode: if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) { if (fc.nodeValue!=vchildren[0]) { fc.nodeValue = vchildren[0]; } } // otherwise, if there are existing or new children, diff them: else if (vchildren && vchildren.length || fc!=null) { innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null); } // Apply attributes/props from VNode to the DOM Element: diffAttributes(out, vnode.attributes, props); // restore previous SVG mode: (in case we're exiting an SVG namespace) isSvgMode = prevSvgMode; return out; } /** * Apply child and attribute changes between a VNode and a DOM Node to the DOM. * @param {import('../dom').PreactElement} dom Element whose children should be compared & mutated * @param {Array} vchildren Array of VNodes to compare to `dom.childNodes` * @param {object} context Implicitly descendant context object (from most * recent `getChildContext()`) * @param {boolean} mountAll Whether or not to immediately mount all components * @param {boolean} isHydrating if `true`, consumes externally created elements * similar to hydration */ function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) { let originalChildren = dom.childNodes, children = [], keyed = {}, keyedLen = 0, min = 0, len = originalChildren.length, childrenLen = 0, vlen = vchildren ? vchildren.length : 0, j, c, f, vchild, child; // Build up a map of keyed children and an Array of unkeyed children: if (len!==0) { for (let i=0; i