diff --git a/src/diff/children.js b/src/diff/children.js index ddf901a298..5ee3d85508 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -306,50 +306,7 @@ function constructNewChildrenArray( } } - if (moved) { - // Children were reordered: mark the minimal set of matched (MATCHED flag) - // children for insertion by finding the longest increasing subsequence of - // old indices (patience sorting). Children on the subsequence stay in - // place, all others get INSERT_VNODE. `_index` still holds the - // matchingIndex here. - /** @type {number[]} tails[x] is the smallest old index ending an increasing subsequence of length x+1 */ - let tails = []; - /** @type {number[]} length of the longest increasing subsequence ending at child i */ - let lisLengths = []; - for (i = 0; i < newChildrenLength; i++) { - childVNode = newChildren[i]; - if (childVNode && childVNode._flags & MATCHED) { - // Binary search for the insertion point, keeping the pass at - // O(n log n) even for pathological reorders. - let lo = 0, - hi = tails.length; - while (lo < hi) { - const mid = (lo + hi) >> 1; - if (tails[mid] < childVNode._index) { - lo = mid + 1; - } else { - hi = mid; - } - } - tails[lo] = childVNode._index; - lisLengths[i] = lo + 1; - } - } - - // `skew` is dead after the main loop; reuse it as the remaining - // subsequence length while walking backwards. Likewise `i` is left at - // newChildrenLength by the loop above. - skew = tails.length; - while (i--) { - if (lisLengths[i]) { - if (lisLengths[i] == skew) { - skew--; - } else { - newChildren[i]._flags |= INSERT_VNODE; - } - } - } - } + if (moved) markMoves(newChildren); // Remove remaining oldChildren if there are any. Loop forwards so that as we // unmount DOM from the beginning of the oldChildren, we can adjust oldDom to @@ -498,3 +455,51 @@ function findMatchingIndex( return -1; } + +/** + * Children were reordered: mark the minimal set of matched (MATCHED flag) + * children for insertion by finding the longest increasing subsequence of + * old indices (patience sorting). Children on the subsequence stay in + * place, all others get INSERT_VNODE. `_index` still holds the + * matchingIndex here. + * @param {VNode[]} newChildren + */ +function markMoves(newChildren) { + /** @type {number[]} tails[x] is the smallest old index ending an increasing subsequence of length x+1 */ + let tails = []; + /** @type {number[]} length of the longest increasing subsequence ending at child i */ + let lisLengths = []; + let i, childVNode; + for (i = 0; i < newChildren.length; i++) { + childVNode = newChildren[i]; + if (childVNode && childVNode._flags & MATCHED) { + // Binary search for the insertion point, keeping the pass at + // O(n log n) even for pathological reorders. + let lo = 0, + hi = tails.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (tails[mid] < childVNode._index) { + lo = mid + 1; + } else { + hi = mid; + } + } + tails[lo] = childVNode._index; + lisLengths[i] = lo + 1; + } + } + + // Walk backwards matching the running subsequence length; `i` is left at + // newChildren.length by the loop above. + let remaining = tails.length; + while (i--) { + if (lisLengths[i]) { + if (lisLengths[i] == remaining) { + remaining--; + } else { + newChildren[i]._flags |= INSERT_VNODE; + } + } + } +} diff --git a/src/diff/index.js b/src/diff/index.js index 951dfb998d..4e7c5f95ed 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -83,26 +83,7 @@ export function diff( (tmp = oldVNode._component._excess) ) { newVNode._flags |= MODE_HYDRATE; - excessDomChildren = []; - if (tmp.nodeType == 8) { - // Re-scan DOM from stored start marker for streamed hydration. - // `depth` only ever reaches 0 through the `break` below, so it - // doesn't need to be re-tested in the loop condition. - for ( - let depth = 1, node = tmp.nextSibling; - node; - node = node.nextSibling - ) { - if (node.nodeType == 8) { - if (node.data.startsWith('$s')) depth++; - else if (node.data.startsWith('/$s') && !--depth) break; - } - excessDomChildren.push(node); - } - } else { - excessDomChildren.push(tmp); - } - oldDom = excessDomChildren[0]; + oldDom = (excessDomChildren = collectExcess(tmp))[0]; oldVNode._component._excess = NULL; } @@ -374,48 +355,10 @@ export function diff( // if hydrating or creating initial tree, bailout preserves DOM: if (isHydrating || excessDomChildren) { if (e.then) { - let commentMarkersToFind = 0, - startMarker; - newVNode._flags |= isHydrating ? MODE_HYDRATE | MODE_SUSPENDED : MODE_SUSPENDED; - - if (excessDomChildren) { - for (let i = 0; i < excessDomChildren.length; i++) { - let child = excessDomChildren[i]; - if (!child) continue; - - if (child.nodeType == 8) { - excessDomChildren[i] = NULL; - if (child.data.startsWith('$s')) { - if (!commentMarkersToFind++) startMarker = child; - } else if ( - child.data.startsWith('/$s') && - !--commentMarkersToFind - ) { - oldDom = child; - break; - } - } else if (commentMarkersToFind) { - excessDomChildren[i] = NULL; - } - } - } - - if (!startMarker) { - while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { - oldDom = oldDom.nextSibling; - } - - if (excessDomChildren) { - excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL; - } - startMarker = oldDom; - } - // Store the start marker directly; children re-scanned on resume - newVNode._component._excess = startMarker; - newVNode._dom = oldDom; + newVNode._dom = suspendVNode(newVNode, excessDomChildren, oldDom); } else if (excessDomChildren) { excessDomChildren.some(removeNode); } @@ -801,3 +744,78 @@ export function unmount(vnode, parentVNode, skipRemove) { function doRender(props, state, context) { return this.constructor(props, context); } + +/** + * Collect the DOM nodes a suspended (streamed) hydration boundary should + * resume against, starting from its stored start marker. + * @param {import('../internal').PreactElement} startMarker + * @returns {import('../internal').PreactElement[]} + */ +function collectExcess(startMarker) { + let excessDomChildren = []; + if (startMarker.nodeType == 8) { + // Re-scan DOM from stored start marker for streamed hydration. + // `depth` only ever reaches 0 through the `break` below, so it + // doesn't need to be re-tested in the loop condition. + for ( + let depth = 1, node = startMarker.nextSibling; + node; + node = node.nextSibling + ) { + if (node.nodeType == 8) { + if (node.data.startsWith('$s')) depth++; + else if (node.data.startsWith('/$s') && !--depth) break; + } + excessDomChildren.push(node); + } + } else { + excessDomChildren.push(startMarker); + } + return excessDomChildren; +} + +/** + * Record where a suspending vnode's DOM starts so the diff can resume there, + * skipping streamed `$s` comment markers. + * @param {import('../internal').VNode} newVNode + * @param {import('../internal').PreactElement[]} excessDomChildren + * @param {import('../internal').PreactElement} oldDom + * @returns {import('../internal').PreactElement} The dom to store on the vnode + */ +function suspendVNode(newVNode, excessDomChildren, oldDom) { + let commentMarkersToFind = 0, + startMarker; + + if (excessDomChildren) { + for (let i = 0; i < excessDomChildren.length; i++) { + let child = excessDomChildren[i]; + if (!child) continue; + + if (child.nodeType == 8) { + excessDomChildren[i] = NULL; + if (child.data.startsWith('$s')) { + if (!commentMarkersToFind++) startMarker = child; + } else if (child.data.startsWith('/$s') && !--commentMarkersToFind) { + oldDom = child; + break; + } + } else if (commentMarkersToFind) { + excessDomChildren[i] = NULL; + } + } + } + + if (!startMarker) { + while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { + oldDom = oldDom.nextSibling; + } + + if (excessDomChildren) { + excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL; + } + startMarker = oldDom; + } + // Store the start marker directly; children re-scanned on resume + newVNode._component._excess = startMarker; + return oldDom; +}