diff --git a/src/diff/children.js b/src/diff/children.js index 80c4f53827..98437e75d4 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -10,6 +10,7 @@ import { import { isArray } from '../util'; import { getDomSibling } from '../component'; import { mount } from './mount'; +import { insert } from './operations'; /** * @typedef {import('../internal').ComponentChildren} ComponentChildren @@ -331,63 +332,6 @@ function constructNewChildrenArray( return oldDom; } -/** - * @param {VNode} parentVNode - * @param {PreactElement} oldDom - * @param {PreactElement} parentDom - * @returns {PreactElement} - */ -function insert(parentVNode, oldDom, parentDom) { - // Note: VNodes in nested suspended trees may be missing _children. - - if (typeof parentVNode.type == 'function') { - let children = parentVNode._children; - for (let i = 0; children && i < children.length; i++) { - if (children[i]) { - // If we enter this code path on sCU bailout, where we copy - // oldVNode._children to newVNode._children, we need to update the old - // children's _parent pointer to point to the newVNode (parentVNode - // here). - children[i]._parent = parentVNode; - oldDom = insert(children[i], oldDom, parentDom); - } - } - - return oldDom; - } else if (parentVNode._dom != oldDom) { - if (oldDom && parentVNode.type && !parentDom.contains(oldDom)) { - oldDom = getDomSibling(parentVNode); - } - parentDom.insertBefore(parentVNode._dom, oldDom || null); - oldDom = parentVNode._dom; - } - - do { - oldDom = oldDom && oldDom.nextSibling; - } while (oldDom != null && oldDom.nodeType === 8); - - return oldDom; -} - -/** - * Flatten and loop through the children of a virtual node - * @param {ComponentChildren} children The unflattened children of a virtual - * node - * @returns {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; -} - /** * @param {VNode} childVNode * @param {VNode[]} oldChildren diff --git a/src/diff/mount.js b/src/diff/mount.js index 67e066d281..7bf501172b 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -6,8 +6,8 @@ import { UNDEFINED } from '../constants'; import { BaseComponent } from '../component'; -import { Fragment } from '../create-element'; -import { diffChildren } from './children'; +import { createVNode, Fragment } from '../create-element'; +import { insert } from './operations'; import { setProperty } from './props'; import { assign, isArray, slice } from '../util'; import options from '../options'; @@ -155,11 +155,10 @@ export function mount( tmp != null && tmp.type === Fragment && tmp.key == null; let renderResult = isTopLevelFragment ? tmp.props.children : tmp; - oldDom = diffChildren( + oldDom = mountChildren( parentDom, isArray(renderResult) ? renderResult : [renderResult], newVNode, - EMPTY_OBJ, globalContext, namespace, excessDomChildren, @@ -361,11 +360,10 @@ function mountElementNode( newVNode._children = []; } else { - diffChildren( + mountChildren( dom, isArray(newChildren) ? newChildren : [newChildren], newVNode, - EMPTY_OBJ, globalContext, nodeType === 'foreignObject' ? 'http://www.w3.org/1999/xhtml' @@ -415,3 +413,147 @@ function mountElementNode( function doRender(props, _state, context) { return this.constructor(props, context); } + +/** + * Diff the children of a virtual node + * @param {PreactElement} parentDom The DOM element whose children are being + * diffed + * @param {ComponentChildren[]} renderResult + * @param {VNode} newParentVNode The new virtual node whose children should be + * diff'ed against oldParentVNode + * @param {object} globalContext The current context object - modified by + * getChildContext + * @param {string} namespace Current namespace of the DOM node (HTML, SVG, or MathML) + * @param {Array} excessDomChildren + * @param {Array} commitQueue List of components which have callbacks + * to invoke in commitRoot + * @param {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 {any[]} refQueue an array of elements needed to invoke refs + */ +function mountChildren( + parentDom, + renderResult, + newParentVNode, + globalContext, + namespace, + excessDomChildren, + commitQueue, + oldDom, + isHydrating, + refQueue +) { + let i, + /** @type {VNode} */ + childVNode, + /** @type {PreactElement} */ + newDom, + /** @type {PreactElement} */ + firstChildDom; + + let newChildrenLength = renderResult.length; + newParentVNode._children = []; + + for (i = 0; i < newChildrenLength; i++) { + // @ts-expect-error We are reusing the childVNode variable to hold both the + // pre and post normalized childVNode + childVNode = renderResult[i]; + + if ( + childVNode == null || + typeof childVNode == 'boolean' || + typeof childVNode == 'function' + ) { + childVNode = newParentVNode._children[i] = null; + continue; + } + // 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.constructor == String + ) { + childVNode = newParentVNode._children[i] = createVNode( + null, + childVNode, + null, + null, + null + ); + } else if (isArray(childVNode)) { + childVNode = newParentVNode._children[i] = createVNode( + Fragment, + { children: childVNode }, + null, + null, + null + ); + } else if (childVNode.constructor === UNDEFINED && 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; + } + + if (childVNode == null) continue; + + childVNode._index = i; + childVNode._parent = newParentVNode; + childVNode._depth = newParentVNode._depth + 1; + + // Morph the old element into the new one, but don't append it to the dom yet + const result = mount( + parentDom, + childVNode, + globalContext, + namespace, + excessDomChildren, + commitQueue, + oldDom, + isHydrating, + refQueue + ); + + // Adjust DOM nodes + newDom = childVNode._dom; + if (childVNode.ref) { + refQueue.push( + childVNode.ref, + childVNode._component || newDom, + childVNode + ); + } + + if (firstChildDom == null && newDom != null) { + firstChildDom = newDom; + } + + if (typeof childVNode.type != 'function') { + oldDom = insert(childVNode, oldDom, parentDom); + } else if (typeof childVNode.type == 'function' && result !== UNDEFINED) { + oldDom = result; + } else if (newDom) { + oldDom = newDom.nextSibling; + } + } + + newParentVNode._dom = firstChildDom; + + return oldDom; +} diff --git a/src/diff/operations.js b/src/diff/operations.js new file mode 100644 index 0000000000..2369ca4b3c --- /dev/null +++ b/src/diff/operations.js @@ -0,0 +1,59 @@ +import { getDomSibling } from '../component'; +import { isArray } from '../util'; + +/** + * @param {VNode} parentVNode + * @param {PreactElement} oldDom + * @param {PreactElement} parentDom + * @returns {PreactElement} + */ +export function insert(parentVNode, oldDom, parentDom) { + // Note: VNodes in nested suspended trees may be missing _children. + + if (typeof parentVNode.type == 'function') { + let children = parentVNode._children; + for (let i = 0; children && i < children.length; i++) { + if (children[i]) { + // If we enter this code path on sCU bailout, where we copy + // oldVNode._children to newVNode._children, we need to update the old + // children's _parent pointer to point to the newVNode (parentVNode + // here). + children[i]._parent = parentVNode; + oldDom = insert(children[i], oldDom, parentDom); + } + } + + return oldDom; + } else if (parentVNode._dom != oldDom) { + if (oldDom && parentVNode.type && !parentDom.contains(oldDom)) { + oldDom = getDomSibling(parentVNode); + } + parentDom.insertBefore(parentVNode._dom, oldDom || null); + oldDom = parentVNode._dom; + } + + do { + oldDom = oldDom && oldDom.nextSibling; + } while (oldDom != null && oldDom.nodeType === 8); + + return oldDom; +} + +/** + * Flatten and loop through the children of a virtual node + * @param {ComponentChildren} children The unflattened children of a virtual + * node + * @returns {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; +} diff --git a/src/diff/patch.js b/src/diff/patch.js index 338598635b..7930b0a61b 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -8,11 +8,11 @@ import { UNDEFINED, XHTML_NAMESPACE } from '../constants'; -import { BaseComponent, getDomSibling } from '../component'; +import { getDomSibling } from '../component'; import { Fragment } from '../create-element'; import { diffChildren } from './children'; import { setProperty } from './props'; -import { assign, isArray, slice } from '../util'; +import { assign, isArray } from '../util'; import options from '../options'; /** @@ -248,7 +248,7 @@ export function diff( } catch (e) { newVNode._original = null; // if hydrating or creating initial tree, bailout preserves DOM: - if (isHydrating || excessDomChildren != null) { + if (isHydrating) { if (e.then) { newVNode._flags |= isHydrating ? MODE_HYDRATE | MODE_SUSPENDED @@ -271,10 +271,7 @@ export function diff( } options._catchError(e, newVNode, oldVNode); } - } else if ( - excessDomChildren == null && - newVNode._original === oldVNode._original - ) { + } else if (newVNode._original === oldVNode._original) { newVNode._children = oldVNode._children; newVNode._dom = oldVNode._dom; } else { @@ -532,8 +529,3 @@ export function unmount(vnode, parentVNode, skipRemove) { vnode._component = vnode._parent = vnode._dom = UNDEFINED; } - -/** The `.render()` method for a PFC backing instance. */ -function doRender(props, state, context) { - return this.constructor(props, context); -} diff --git a/src/index.js b/src/index.js index e9cb50fd8e..8cf94e9eab 100644 --- a/src/index.js +++ b/src/index.js @@ -9,5 +9,5 @@ export { export { BaseComponent as Component } from './component'; export { cloneElement } from './clone-element'; export { createContext } from './create-context'; -export { toChildArray } from './diff/children'; +export { toChildArray } from './diff/operations'; export { default as options } from './options';