Skip to content

Commit 6dd8a97

Browse files
authored
Merge pull request #7401 from QwikDev/v2-remove-v-siblings
chore: refactor vSiblings
2 parents ed57e4d + 8da2884 commit 6dd8a97

File tree

7 files changed

+130
-97
lines changed

7 files changed

+130
-97
lines changed

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 38 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ export const vnode_diff = (
136136
/// When elements have keys they can be consumed out of order and therefore we can't use nextSibling.
137137
/// In such a case this array will contain the elements after the current location.
138138
/// The array even indices will contains keys and odd indices the vNode.
139-
let vSiblings: Array<string | null | VNode> | null = null; // See: `SiblingsArray`
140-
let vSiblingsIdx = -1;
139+
let vSiblings: Map<string, VNode> | null = null;
140+
let vSiblingsArray: Array<string | VNode | null> | null = null;
141141

142142
/// Current set of JSX children.
143143
let jsxChildren: JSXChildren[] = null!;
@@ -278,16 +278,9 @@ export const vnode_diff = (
278278
* order and can't rely on `vnode_getNextSibling` and instead we need to go by `vSiblings`.
279279
*/
280280
function peekNextSibling() {
281-
if (vSiblings !== null) {
282-
// We came across a key, and we moved nodes around. This means we can no longer use
283-
// `vnode_getNextSibling` to look at next node and instead we have to go by `vSiblings`.
284-
const idx = vSiblingsIdx + SiblingsArray.NextVNode;
285-
return idx < vSiblings.length ? (vSiblings[idx] as any) : null;
286-
} else {
287-
// If we don't have a `vNewNode`, than that means we just reconciled the current node.
288-
// So advance it.
289-
return vCurrent ? vnode_getNextSibling(vCurrent) : null;
290-
}
281+
// If we don't have a `vNewNode`, than that means we just reconciled the current node.
282+
// So advance it.
283+
return vCurrent ? vnode_getNextSibling(vCurrent) : null;
291284
}
292285

293286
/**
@@ -299,9 +292,6 @@ export const vnode_diff = (
299292
*/
300293
function advanceToNextSibling() {
301294
vCurrent = peekNextSibling();
302-
if (vSiblings !== null) {
303-
vSiblingsIdx += SiblingsArray.Size; // advance;
304-
}
305295
}
306296

307297
/**
@@ -332,7 +322,7 @@ export const vnode_diff = (
332322
if (descendVNode) {
333323
assertDefined(vCurrent || vNewNode, 'Expecting vCurrent to be defined.');
334324
vSiblings = null;
335-
vSiblingsIdx = -1;
325+
vSiblingsArray = null;
336326
vParent = vNewNode || vCurrent!;
337327
vCurrent = vnode_getFirstChild(vParent);
338328
vNewNode = null;
@@ -343,8 +333,8 @@ export const vnode_diff = (
343333
function ascend() {
344334
const descendVNode = stack.pop(); // boolean: descendVNode
345335
if (descendVNode) {
346-
vSiblingsIdx = stack.pop();
347336
vSiblings = stack.pop();
337+
vSiblingsArray = stack.pop();
348338
vNewNode = stack.pop();
349339
vCurrent = stack.pop();
350340
vParent = stack.pop();
@@ -359,7 +349,7 @@ export const vnode_diff = (
359349
function stackPush(children: JSXChildren, descendVNode: boolean) {
360350
stack.push(jsxChildren, jsxIdx, jsxCount, jsxValue);
361351
if (descendVNode) {
362-
stack.push(vParent, vCurrent, vNewNode, vSiblings, vSiblingsIdx);
352+
stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSiblings);
363353
}
364354
stack.push(descendVNode);
365355
if (Array.isArray(children)) {
@@ -384,9 +374,6 @@ export const vnode_diff = (
384374
function getInsertBefore() {
385375
if (vNewNode) {
386376
return vCurrent;
387-
} else if (vSiblings !== null) {
388-
const nextIdx = vSiblingsIdx + SiblingsArray.NextVNode;
389-
return nextIdx < vSiblings.length ? (vSiblings[nextIdx] as VNode) : null;
390377
} else {
391378
return peekNextSibling();
392379
}
@@ -751,10 +738,6 @@ export const vnode_diff = (
751738
vCurrent = vNewNode;
752739
// We need to clean up the vNewNode, because we don't want to skip advance to next sibling (see `advance` function).
753740
vNewNode = null;
754-
// We need also to go back to the previous sibling, because we assigned previous sibling to the vCurrent.
755-
if (vSiblings !== null) {
756-
vSiblingsIdx -= SiblingsArray.Size;
757-
}
758741
}
759742
}
760743
// reconcile attributes
@@ -959,60 +942,56 @@ export const vnode_diff = (
959942
}
960943

961944
/**
962-
* Retrieve the child with the given key.
963-
*
964-
* By retrieving the child with the given key we are effectively removing it from the list of
965-
* future elements. This means that we can't just use `vnode_getNextSibling` to find the next
966-
* instead we have to keep track of the elements we have already seen.
967-
*
968-
* We call this materializing the elements.
969-
*
970-
* `vSiblingsIdx`:
971-
*
972-
* - -1: Not materialized
973-
* - Positive number - the index of the next element in the `vSiblings` array.
945+
* This function is used to retrieve the child with the given key. If the child is not found, it
946+
* will return null.
974947
*
975-
* By retrieving the child with the given key we are effectively removing it from the list (hence
976-
* we need to splice the `vSiblings` array).
948+
* After finding the first child with the given key we will create a map of all the keyed siblings
949+
* and an array of non-keyed siblings. This is done to optimize the search for the next child with
950+
* the specified key.
977951
*
978-
* @param nodeName
979-
* @param key
980-
* @returns Array where: (see: `SiblingsArray`)
981-
*
982-
* - Idx%3 == 0 nodeName
983-
* - Idx%3 == 1 key
984-
* - Idx%3 == 2 vNode
952+
* @param nodeName - The name of the node.
953+
* @param key - The key of the node.
954+
* @returns The child with the given key or null if not found.
985955
*/
986956
function retrieveChildWithKey(
987957
nodeName: string | null,
988958
key: string | null
989959
): ElementVNode | VirtualVNode | null {
990960
let vNodeWithKey: ElementVNode | VirtualVNode | null = null;
991-
if (vSiblingsIdx === -1) {
961+
if (vSiblings === null) {
992962
// it is not materialized; so materialize it.
993-
vSiblings = [];
994-
vSiblingsIdx = 0;
963+
vSiblings = new Map<string, VNode>();
964+
vSiblingsArray = [];
995965
let vNode = vCurrent;
996966
while (vNode) {
997967
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
998968
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
999969
if (vNodeWithKey === null && vKey == key && name == nodeName) {
1000970
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
1001971
} else {
1002-
// we only add the elements which we did not find yet.
1003-
vSiblings.push(name, vKey, vNode);
972+
if (vKey === null) {
973+
vSiblingsArray.push(name, vNode);
974+
} else {
975+
// we only add the elements which we did not find yet.
976+
vSiblings.set(name + ':' + vKey, vNode);
977+
}
1004978
}
1005979
vNode = vnode_getNextSibling(vNode);
1006980
}
1007981
} else {
1008-
for (let idx = vSiblingsIdx; idx < vSiblings!.length; idx += SiblingsArray.Size) {
1009-
const name = vSiblings![idx + SiblingsArray.Name];
1010-
const vKey = vSiblings![idx + SiblingsArray.Key];
1011-
if (vKey === key && name === nodeName) {
1012-
vNodeWithKey = vSiblings![idx + SiblingsArray.VNode] as any;
1013-
// remove the node from the siblings array
1014-
vSiblings?.splice(idx, SiblingsArray.Size);
1015-
break;
982+
if (key === null) {
983+
for (let i = 0; i < vSiblingsArray!.length; i += 2) {
984+
if (vSiblingsArray![i] === nodeName) {
985+
vNodeWithKey = vSiblingsArray![i + 1] as ElementVNode | VirtualVNode;
986+
vSiblingsArray!.splice(i, 2);
987+
break;
988+
}
989+
}
990+
} else {
991+
const vSibling = vSiblings.get(nodeName + ':' + key);
992+
if (vSibling) {
993+
vNodeWithKey = vSibling as ElementVNode | VirtualVNode;
994+
vSiblings.delete(nodeName + ':' + key);
1016995
}
1017996
}
1018997
}
@@ -1451,10 +1430,3 @@ function markVNodeAsDeleted(vCursor: VNode) {
14511430
*/
14521431
export const HANDLER_PREFIX = ':';
14531432
let count = 0;
1454-
const enum SiblingsArray {
1455-
Name = 0,
1456-
Key = 1,
1457-
VNode = 2,
1458-
Size = 3,
1459-
NextVNode = Size + VNode,
1460-
}

packages/qwik/src/core/client/vnode-diff.unit.tsx

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,18 @@ describe('vNode-diff', () => {
412412
describe('keys', () => {
413413
it('should not reuse element because old has a key and new one does not', () => {
414414
const { vNode, vParent, document } = vnode_fromJSX(
415-
<test key="KA_6">
416-
<b {...{ 'q:key': '1' }}>old</b>
417-
</test>
415+
_jsxSorted('test', {}, null, [_jsxSorted('b', {}, null, 'old', 0, '1')], 0, 'KA_6')
418416
);
419-
const test = (
420-
<test key="KA_6">
421-
<b>new</b>
422-
</test>
417+
const test = _jsxSorted(
418+
'test',
419+
{},
420+
null,
421+
[_jsxSorted('b', {}, null, 'new', 0, null)],
422+
0,
423+
'KA_6'
423424
);
424-
const bOriginal = document.querySelector('b[key=1]')!;
425+
const bOriginal = document.querySelector('b[q\\:key=1]')!;
426+
expect(bOriginal).toBeDefined();
425427
vnode_diff(
426428
{ $journal$: journal, $scheduler$: scheduler, document } as any,
427429
test,
@@ -431,32 +433,46 @@ describe('vNode-diff', () => {
431433
vnode_applyJournal(journal);
432434
expect(vNode).toMatchVDOM(test);
433435
const bSecond = document.querySelector('b')!;
436+
expect(bSecond).toBeDefined();
434437
expect(bSecond).not.toBe(bOriginal);
435438
});
436439
it('should reuse elements if keys match', () => {
437440
const { vNode, vParent, document } = vnode_fromJSX(
438-
<test key="KA_6">
439-
<b {...{ 'q:key': '1' }}>1</b>
440-
<b {...{ 'q:key': '2' }}>2</b>
441-
</test>
442-
);
443-
const test = (
444-
<test key="KA_6">
445-
<b>before</b>
446-
<b key="2">2</b>
447-
<b key="3">3</b>
448-
<b>in</b>
449-
<b key="1">1</b>
450-
<b>after</b>
451-
</test>
441+
_jsxSorted(
442+
'test',
443+
{},
444+
null,
445+
[_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, '2')],
446+
0,
447+
'KA_6'
448+
)
452449
);
453-
const b1 = document.querySelector('b[key=1]')!;
454-
const b2 = document.querySelector('b[key=1]')!;
450+
const test = _jsxSorted(
451+
'test',
452+
{},
453+
null,
454+
[
455+
_jsxSorted('b', {}, null, 'before', 0, null),
456+
_jsxSorted('b', {}, null, '2', 0, '2'),
457+
_jsxSorted('b', {}, null, '3', 0, '3'),
458+
_jsxSorted('b', {}, null, 'in', 0, null),
459+
_jsxSorted('b', {}, null, '1', 0, '1'),
460+
_jsxSorted('b', {}, null, 'after', 0, null),
461+
],
462+
0,
463+
'KA_6'
464+
);
465+
const selectB1 = () => document.querySelector('b[q\\:key=1]')!;
466+
const selectB2 = () => document.querySelector('b[q\\:key=2]')!;
467+
const b1 = selectB1();
468+
const b2 = selectB2();
469+
expect(b1).toBeDefined();
470+
expect(b2).toBeDefined();
455471
vnode_diff({ $journal$: journal, document } as any, test, vParent, null);
456472
vnode_applyJournal(journal);
457473
expect(vNode).toMatchVDOM(test);
458-
expect(b1).toBe(document.querySelector('b[key=1]')!);
459-
expect(b2).toBe(document.querySelector('b[key=2]')!);
474+
expect(b1).toBe(selectB1());
475+
expect(b2).toBe(selectB2());
460476
});
461477
});
462478
describe.todo('fragments', () => {});

packages/qwik/src/core/tests/component.spec.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,52 @@ describe.each([
823823
);
824824
});
825825

826+
it('should preserve the same elements', async () => {
827+
const Cmp = component$(() => {
828+
const keys = useSignal(['A', 'B', 'C']);
829+
830+
return (
831+
<div onClick$={() => (keys.value = ['B', 'C', 'A'])}>
832+
{keys.value.map((key) => (
833+
<span key={key} id={key}>
834+
{key}
835+
</span>
836+
))}
837+
</div>
838+
);
839+
});
840+
841+
const { vNode, document } = await render(<Cmp />, { debug });
842+
expect(vNode).toMatchVDOM(
843+
<Component>
844+
<div>
845+
<span id="A">A</span>
846+
<span id="B">B</span>
847+
<span id="C">C</span>
848+
</div>
849+
</Component>
850+
);
851+
const a1 = document.getElementById('A');
852+
const b1 = document.getElementById('B');
853+
const c1 = document.getElementById('C');
854+
await trigger(document.body, 'div', 'click');
855+
expect(vNode).toMatchVDOM(
856+
<Component>
857+
<div>
858+
<span id="B">B</span>
859+
<span id="C">C</span>
860+
<span id="A">A</span>
861+
</div>
862+
</Component>
863+
);
864+
const a2 = document.getElementById('A');
865+
const b2 = document.getElementById('B');
866+
const c2 = document.getElementById('C');
867+
expect(a1).toBe(a2);
868+
expect(b1).toBe(b2);
869+
expect(c1).toBe(c2);
870+
});
871+
826872
it('should render correctly component only with text node and node sibling', async () => {
827873
const Child = component$(() => {
828874
return <>0</>;

starters/apps/e2e/src/components/attributes/attributes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const Attributes = component$(() => {
1818
>
1919
Rerender
2020
</button>
21+
<span id="render-count">{render.value}</span>
2122
<AttributesChild v={render.value} key={render.value} />
2223
<ProgressParent />
2324
</>

starters/apps/e2e/src/components/context/context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const ContextRoot = component$(() => {
2828
<button id="btn-rerender" onClick$={() => count.value++}>
2929
Client Rerender
3030
</button>
31+
<span id="render-count">{count.value}</span>
3132
<ContextApp key={count.value} />
3233
</div>
3334
);

starters/e2e/e2e.attributes.e2e.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,8 @@ test.describe("attributes", () => {
325325
test.describe("client rerender", () => {
326326
test.beforeEach(async ({ page }) => {
327327
const toggleRender = page.locator("#force-rerender");
328-
const v = Number(await toggleRender.getAttribute("data-v"));
329328
await toggleRender.click();
330-
await expect(page.locator("#renderCount")).toHaveText(`Render ${v}`);
331-
// Without this the tests still fail?
332-
await page.waitForTimeout(50);
329+
await expect(page.locator("#render-count")).toHaveText("1");
333330
});
334331
tests();
335332
});

starters/e2e/e2e.context.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ test.describe("context", () => {
189189
test.beforeEach(async ({ page }) => {
190190
const rerender = page.locator("#btn-rerender");
191191
await rerender.click();
192-
await page.waitForTimeout(100);
192+
await expect(page.locator("#render-count")).toHaveText("1");
193193
});
194194
tests(false);
195195
});

0 commit comments

Comments
 (0)