Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 49 additions & 44 deletions src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
}
}
136 changes: 77 additions & 59 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Loading