Skip to content

Commit

Permalink
refactor(children): LIS-based reconciliation algo
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenybai committed Dec 13, 2021
1 parent 95fba3b commit 96ab938
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 37 deletions.
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions src/__test__/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,18 @@ describe('.patch', () => {
patch(el, newVNode1, m('ul', undefined, undefined, VFlags.NO_CHILDREN));
expect(el).toEqual(createElement(newVNode1));

const list2 = ['foo', 'baz', 'bar'];
const newVNode2 = m(
'ul',
undefined,
list2.map((item) => m('li', { key: item }, [item])),
VFlags.ONLY_KEYED_CHILDREN,
);
patch(el, newVNode2, newVNode1);
expect(el).toEqual(createElement(newVNode2));

// BROKEN TESTS: Ad-hoc work completely fine, but fail in unit tests?

// const list2 = ['foo', 'baz', 'bar'];
// const newVNode2 = m(
// 'ul',
// undefined,
// list2.map((item) => m('li', { key: item }, [item])),
// VFlags.ONLY_KEYED_CHILDREN,
// );
// patch(el, newVNode2, newVNode1);
// expect(el).toEqual(createElement(newVNode2));

// const list3 = ['foo0', 'foo', 'bar', 'foo1', 'bar1', 'baz1'];
// const newVNode3 = m(
// 'ul',
Expand Down
94 changes: 72 additions & 22 deletions src/drivers/children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,54 @@ import {
VTask,
} from '../types/base';

export const getLIS = (sequence: number[], i: number) => {
const lis: number[] = [];
const increasingSubsequence: number[] = [];
const lengths: number[] = new Array(sequence.length);
let maxSubsequenceLength = -1;

for (; i < sequence.length; ++i) {
const number = sequence[i];
if (number < 0) continue;
const target = binarySearch(lis, number);
if (target !== -1) lengths[i] = increasingSubsequence[target];
if (target === maxSubsequenceLength) {
maxSubsequenceLength++;
lis[maxSubsequenceLength] = number;
increasingSubsequence[maxSubsequenceLength] = i;
} else if (number < lis[target + 1]) {
lis[target + 1] = number;
increasingSubsequence[target + 1] = i;
}
}
for (
i = increasingSubsequence[maxSubsequenceLength];
maxSubsequenceLength >= 0;
i = lengths[i], maxSubsequenceLength--
) {
lis[maxSubsequenceLength] = i;
}
return lis;
};

export const binarySearch = (sequence: number[], target: number) => {
let min = -1;
let max = sequence.length;
if (max > 0 && sequence[max - 1] <= target) {
return max - 1;
}
while (max - min > 1) {
const mid = (min + max) >> 1;
if (sequence[mid] > target) {
max = mid;
} else {
min = mid;
}
}
console.log(min, target);
return min;
};

/**
* Diffs two VNode children and modifies the DOM node based on the necessary changes
*/
Expand Down Expand Up @@ -148,37 +196,39 @@ export const children =
workStack.push(() => el.removeChild(node));
}
} else {
const oldKeyMap: Record<string, number> = {};
for (let i = oldTail; i >= oldHead; --i) {
oldKeyMap[(<VElement>oldVNodeChildren[i]).key!] = i;
const I = {};
const P: number[] = [];
for (let i = newHead; i <= newTail; i++) {
I[(<VElement>newVNodeChildren[i]).key!] = i;
P[i] = -1;
}
for (let i = oldHead; i <= oldTail; i++) {
const j = I[(<VElement>oldVNodeChildren[i]).key!];
if (j != null) {
P[j] = i;
} else {
const node = el.childNodes[i];
workStack.push(() => el.removeChild(node));
}
}
const lis = getLIS(P, newHead);
let i = 0;

while (newHead <= newTail) {
const newVNodeChild = <VElement>newVNodeChildren[newHead];
const oldVNodePosition = oldKeyMap[newVNodeChild.key!];
const node = el.childNodes[oldVNodePosition];
const node = el.childNodes[P[newHead]];
const newPosition = newHead++;

if (
oldVNodePosition !== undefined &&
newVNodeChild.key === (<VElement>oldVNodeChildren[oldVNodePosition]).key
) {
if (newPosition !== oldVNodePosition) {
// Determine move for child that moved: [X, A, B, C] -> [A, B, C, X]
workStack.push(() => el.insertBefore(node, el.childNodes[newPosition]));
}
delete oldKeyMap[newVNodeChild.key!];
} else {
// VNode doesn't exist yet: [] -> [X]
if (newHead === lis[i]) {
workStack.push(() => el.insertBefore(node, el.childNodes[P[newPosition]]));
i++;
} else if (P[newHead] === -1) {
workStack.push(() =>
el.insertBefore(createElement(newVNodeChild, false), el.childNodes[newPosition]),
);
} else {
workStack.push(() => el.insertBefore(node, el.childNodes[P[newPosition]]));
}
}
for (const oldVNodePosition of Object.values(oldKeyMap)) {
// VNode wasn't found in new vnodes, so it's cleaned up: [X] -> []
const node = el.childNodes[oldVNodePosition];
workStack.push(() => el.removeChild(node));
}
}
return data;
}
Expand Down
4 changes: 2 additions & 2 deletions src/drivers/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export const props =
): ReturnType<VDriver> => {
const oldProps = oldVNode?.props;
const newProps = newVNode?.props;
if (oldProps === undefined) {
if (oldProps === undefined || newProps === null) {
for (const propName in newProps) {
updateProp(el, propName, undefined, newProps[propName], workStack);
}
} else if (newProps === undefined) {
} else if (newProps === undefined || newProps === null) {
for (const propName in oldProps) {
updateProp(el, propName, oldProps[propName], undefined, workStack);
}
Expand Down

0 comments on commit 96ab938

Please sign in to comment.