import { diff, unmount, applyRef } from './index'; import { createVNode, Fragment } from '../create-element'; import { EMPTY_OBJ, EMPTY_ARR } from '../constants'; import { isArray } from '../util'; import { getDomSibling } from '../component'; /** * Diff the children of a virtual node * @param {import('../internal').PreactElement} parentDom The DOM element whose * children are being diffed * @param {import('../internal').ComponentChildren[]} renderResult * @param {import('../internal').VNode} newParentVNode The new virtual * node whose children should be diff'ed against oldParentVNode * @param {import('../internal').VNode} oldParentVNode The old virtual * node whose children should be diff'ed against newParentVNode * @param {object} globalContext The current context object - modified by getChildContext * @param {boolean} isSvg Whether or not this DOM node is an SVG node * @param {Array} excessDomChildren * @param {Array} commitQueue List of components * which have callbacks to invoke in commitRoot * @param {import('../internal').PreactElement} oldDom The current attached DOM * element any new dom elements should be placed around. Likely `null` on first * render (except when hydrating). Can be a sibling DOM element when diffing * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. * @param {boolean} isHydrating Whether or not we are in hydration * @param {Array} refQueue an array of elements needed to invoke refs */ export function diffChildren( parentDom, renderResult, newParentVNode, oldParentVNode, globalContext, isSvg, excessDomChildren, commitQueue, oldDom, isHydrating, refQueue ) { let i, j, oldVNode, childVNode, newDom, firstChildDom, skew = 0; // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR // as EMPTY_OBJ._children should be `undefined`. let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; let oldChildrenLength = oldChildren.length, remainingOldChildren = oldChildrenLength, newChildrenLength = renderResult.length; newParentVNode._children = []; for (i = 0; i < newChildrenLength; i++) { childVNode = renderResult[i]; if ( childVNode == null || typeof childVNode == 'boolean' || typeof childVNode == 'function' ) { childVNode = newParentVNode._children[i] = null; } // If this newVNode is being reused (e.g.
{reuse}{reuse}
) in the same diff, // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have // it's own DOM & etc. pointers else if ( typeof childVNode == 'string' || typeof childVNode == 'number' || // eslint-disable-next-line valid-typeof typeof childVNode == 'bigint' ) { childVNode = newParentVNode._children[i] = createVNode( null, childVNode, null, null, childVNode ); } else if (isArray(childVNode)) { childVNode = newParentVNode._children[i] = createVNode( Fragment, { children: childVNode }, null, null, null ); } else if (childVNode._depth > 0) { // VNode is already in use, clone it. This can happen in the following // scenario: // const reuse =
//
{reuse}{reuse}
childVNode = newParentVNode._children[i] = createVNode( childVNode.type, childVNode.props, childVNode.key, childVNode.ref ? childVNode.ref : null, childVNode._original ); } else { childVNode = newParentVNode._children[i] = childVNode; } // Terser removes the `continue` here and wraps the loop body // in a `if (childVNode) { ... } condition if (childVNode == null) { oldVNode = oldChildren[i]; if (oldVNode && oldVNode.key == null && oldVNode._dom) { if (oldVNode._dom == oldDom) { oldVNode._parent = oldParentVNode; oldDom = getDomSibling(oldVNode); } unmount(oldVNode, oldVNode, false); oldChildren[i] = null; } continue; } childVNode._parent = newParentVNode; childVNode._depth = newParentVNode._depth + 1; let skewedIndex = i + skew; const matchingIndex = findMatchingIndex( childVNode, oldChildren, skewedIndex, remainingOldChildren ); if (matchingIndex === -1) { oldVNode = EMPTY_OBJ; } else { oldVNode = oldChildren[matchingIndex] || EMPTY_OBJ; oldChildren[matchingIndex] = undefined; remainingOldChildren--; } // Morph the old element into the new one, but don't append it to the dom yet diff( parentDom, childVNode, oldVNode, globalContext, isSvg, excessDomChildren, commitQueue, oldDom, isHydrating, refQueue ); newDom = childVNode._dom; if ((j = childVNode.ref) && oldVNode.ref != j) { if (oldVNode.ref) { applyRef(oldVNode.ref, null, childVNode); } refQueue.push(j, childVNode._component || newDom, childVNode); } if (newDom != null) { if (firstChildDom == null) { firstChildDom = newDom; } let isMounting = oldVNode === EMPTY_OBJ || oldVNode._original === null; if (isMounting) { if (matchingIndex == -1) { skew--; } } else if (matchingIndex !== skewedIndex) { if (matchingIndex === skewedIndex + 1) { skew++; } else if (matchingIndex > skewedIndex) { if (remainingOldChildren > newChildrenLength - skewedIndex) { skew += matchingIndex - skewedIndex; } else { // ### Change from keyed: I think this was missing from the algo... skew--; } } else if (matchingIndex < skewedIndex) { if (matchingIndex == skewedIndex - 1) { skew = matchingIndex - skewedIndex; } else { skew = 0; } } else { skew = 0; } } skewedIndex = i + skew; if ( typeof childVNode.type == 'function' && (matchingIndex !== skewedIndex || oldVNode._children === childVNode._children) ) { oldDom = reorderChildren(childVNode, oldDom, parentDom); } else if ( typeof childVNode.type != 'function' && (matchingIndex !== skewedIndex || isMounting) ) { oldDom = placeChild(parentDom, newDom, oldDom); } else if (childVNode._nextDom !== undefined) { // Only Fragments or components that return Fragment like VNodes will // have a non-undefined _nextDom. Continue the diff from the sibling // of last DOM child of this child VNode oldDom = childVNode._nextDom; // Eagerly cleanup _nextDom. We don't need to persist the value because // it is only used by `diffChildren` to determine where to resume the diff after // diffing Components and Fragments. Once we store it the nextDOM local var, we // can clean up the property childVNode._nextDom = undefined; } else { oldDom = newDom.nextSibling; } if (typeof newParentVNode.type == 'function') { // Because the newParentVNode is Fragment-like, we need to set it's // _nextDom property to the nextSibling of its last child DOM node. // // `oldDom` contains the correct value here because if the last child // is a Fragment-like, then oldDom has already been set to that child's _nextDom. // If the last child is a DOM VNode, then oldDom will be set to that DOM // node's nextSibling. newParentVNode._nextDom = oldDom; } } } newParentVNode._dom = firstChildDom; // Remove remaining oldChildren if there are any. for (i = oldChildrenLength; i--; ) { if (oldChildren[i] != null) { if ( typeof newParentVNode.type == 'function' && oldChildren[i]._dom != null && oldChildren[i]._dom == newParentVNode._nextDom ) { // If the newParentVNode.__nextDom points to a dom node that is about to // be unmounted, then get the next sibling of that vnode and set // _nextDom to it newParentVNode._nextDom = oldChildren[i]._dom.nextSibling; } unmount(oldChildren[i], oldChildren[i]); } } } function reorderChildren(childVNode, oldDom, parentDom) { // Note: VNodes in nested suspended trees may be missing _children. let c = childVNode._children; let tmp = 0; for (; c && tmp < c.length; tmp++) { let vnode = c[tmp]; if (vnode) { // We typically enter this code path on sCU bailout, where we copy // oldVNode._children to newVNode._children. If that is the case, we need // to update the old children's _parent pointer to point to the newVNode // (childVNode here). vnode._parent = childVNode; if (typeof vnode.type == 'function') { oldDom = reorderChildren(vnode, oldDom, parentDom); } else { oldDom = placeChild(parentDom, vnode._dom, oldDom); } } } return oldDom; } /** * Flatten and loop through the children of a virtual node * @param {import('../index').ComponentChildren} children The unflattened * children of a virtual node * @returns {import('../internal').VNode[]} */ export function toChildArray(children, out) { out = out || []; if (children == null || typeof children == 'boolean') { } else if (isArray(children)) { children.some(child => { toChildArray(child, out); }); } else { out.push(children); } return out; } function placeChild(parentDom, newDom, oldDom) { if (oldDom == null || oldDom.parentNode !== parentDom) { parentDom.insertBefore(newDom, null); } else if (newDom != oldDom || newDom.parentNode == null) { parentDom.insertBefore(newDom, oldDom); } return newDom.nextSibling; } /** * @param {import('../internal').VNode | string} childVNode * @param {import('../internal').VNode[]} oldChildren * @param {number} skewedIndex * @param {number} remainingOldChildren * @returns {number} */ function findMatchingIndex( childVNode, oldChildren, skewedIndex, remainingOldChildren ) { const key = childVNode.key; const type = childVNode.type; let x = skewedIndex - 1; let y = skewedIndex + 1; let oldVNode = oldChildren[skewedIndex]; if ( oldVNode === null || (oldVNode && key == oldVNode.key && type === oldVNode.type) ) { return skewedIndex; } else if (remainingOldChildren > (oldVNode != null ? 1 : 0)) { while (x >= 0 || y < oldChildren.length) { if (x >= 0) { oldVNode = oldChildren[x]; if (oldVNode && key == oldVNode.key && type === oldVNode.type) { return x; } x--; } if (y < oldChildren.length) { oldVNode = oldChildren[y]; if (oldVNode && key == oldVNode.key && type === oldVNode.type) { return y; } y++; } } } return -1; }