Skip to content

Commit 8da2884

Browse files
committed
chore: change vSiblings from array to map
1 parent 54ecdb3 commit 8da2884

File tree

3 files changed

+106
-38
lines changed

3 files changed

+106
-38
lines changed

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

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ export const vnode_diff = (
133133
/// and is not connected to the tree.
134134
let vNewNode: VNode | null = null;
135135

136+
/// When elements have keys they can be consumed out of order and therefore we can't use nextSibling.
137+
/// In such a case this array will contain the elements after the current location.
138+
/// The array even indices will contains keys and odd indices the vNode.
139+
let vSiblings: Map<string, VNode> | null = null;
140+
let vSiblingsArray: Array<string | VNode | null> | null = null;
141+
136142
/// Current set of JSX children.
137143
let jsxChildren: JSXChildren[] = null!;
138144
// Current JSX child.
@@ -315,6 +321,8 @@ export const vnode_diff = (
315321
stackPush(children, descendVNode);
316322
if (descendVNode) {
317323
assertDefined(vCurrent || vNewNode, 'Expecting vCurrent to be defined.');
324+
vSiblings = null;
325+
vSiblingsArray = null;
318326
vParent = vNewNode || vCurrent!;
319327
vCurrent = vnode_getFirstChild(vParent);
320328
vNewNode = null;
@@ -325,6 +333,8 @@ export const vnode_diff = (
325333
function ascend() {
326334
const descendVNode = stack.pop(); // boolean: descendVNode
327335
if (descendVNode) {
336+
vSiblings = stack.pop();
337+
vSiblingsArray = stack.pop();
328338
vNewNode = stack.pop();
329339
vCurrent = stack.pop();
330340
vParent = stack.pop();
@@ -339,7 +349,7 @@ export const vnode_diff = (
339349
function stackPush(children: JSXChildren, descendVNode: boolean) {
340350
stack.push(jsxChildren, jsxIdx, jsxCount, jsxValue);
341351
if (descendVNode) {
342-
stack.push(vParent, vCurrent, vNewNode);
352+
stack.push(vParent, vCurrent, vNewNode, vSiblingsArray, vSiblings);
343353
}
344354
stack.push(descendVNode);
345355
if (Array.isArray(children)) {
@@ -820,7 +830,12 @@ export const vnode_diff = (
820830
value = trackSignalAndAssignHost(value, vnode, key, container, signalData);
821831
}
822832

823-
vnode_setAttr(journal, vnode, key, serializeAttribute(key, value, scopedStyleIdPrefix));
833+
vnode_setAttr(
834+
journal,
835+
vnode,
836+
key,
837+
value !== null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null
838+
);
824839
if (value === null) {
825840
// if we set `null` than attribute was removed and we need to shorten the dstLength
826841
dstLength = dstAttrs.length;
@@ -926,21 +941,59 @@ export const vnode_diff = (
926941
}
927942
}
928943

929-
/** Retrieve the child with the given key. */
944+
/**
945+
* This function is used to retrieve the child with the given key. If the child is not found, it
946+
* will return null.
947+
*
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.
951+
*
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.
955+
*/
930956
function retrieveChildWithKey(
931957
nodeName: string | null,
932958
key: string | null
933959
): ElementVNode | VirtualVNode | null {
934960
let vNodeWithKey: ElementVNode | VirtualVNode | null = null;
935-
let vNode = vCurrent;
936-
while (vNode) {
937-
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
938-
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
939-
if (vKey == key && name == nodeName) {
940-
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
941-
break;
961+
if (vSiblings === null) {
962+
// it is not materialized; so materialize it.
963+
vSiblings = new Map<string, VNode>();
964+
vSiblingsArray = [];
965+
let vNode = vCurrent;
966+
while (vNode) {
967+
const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null;
968+
const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$);
969+
if (vNodeWithKey === null && vKey == key && name == nodeName) {
970+
vNodeWithKey = vNode as ElementVNode | VirtualVNode;
971+
} else {
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+
}
978+
}
979+
vNode = vnode_getNextSibling(vNode);
980+
}
981+
} else {
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);
995+
}
942996
}
943-
vNode = vnode_getNextSibling(vNode);
944997
}
945998
return vNodeWithKey;
946999
}

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

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -409,16 +409,18 @@ describe('vNode-diff', () => {
409409
describe('keys', () => {
410410
it('should not reuse element because old has a key and new one does not', () => {
411411
const { vNode, vParent, document } = vnode_fromJSX(
412-
<test key="KA_6">
413-
<b {...{ 'q:key': '1' }}>old</b>
414-
</test>
412+
_jsxSorted('test', {}, null, [_jsxSorted('b', {}, null, 'old', 0, '1')], 0, 'KA_6')
415413
);
416-
const test = (
417-
<test key="KA_6">
418-
<b>new</b>
419-
</test>
414+
const test = _jsxSorted(
415+
'test',
416+
{},
417+
null,
418+
[_jsxSorted('b', {}, null, 'new', 0, null)],
419+
0,
420+
'KA_6'
420421
);
421-
const bOriginal = document.querySelector('b[key=1]')!;
422+
const bOriginal = document.querySelector('b[q\\:key=1]')!;
423+
expect(bOriginal).toBeDefined();
422424
vnode_diff(
423425
{ $journal$: journal, $scheduler$: scheduler, document } as any,
424426
test,
@@ -428,32 +430,46 @@ describe('vNode-diff', () => {
428430
vnode_applyJournal(journal);
429431
expect(vNode).toMatchVDOM(test);
430432
const bSecond = document.querySelector('b')!;
433+
expect(bSecond).toBeDefined();
431434
expect(bSecond).not.toBe(bOriginal);
432435
});
433436
it('should reuse elements if keys match', () => {
434437
const { vNode, vParent, document } = vnode_fromJSX(
435-
<test key="KA_6">
436-
<b {...{ 'q:key': '1' }}>1</b>
437-
<b {...{ 'q:key': '2' }}>2</b>
438-
</test>
439-
);
440-
const test = (
441-
<test key="KA_6">
442-
<b>before</b>
443-
<b key="2">2</b>
444-
<b key="3">3</b>
445-
<b>in</b>
446-
<b key="1">1</b>
447-
<b>after</b>
448-
</test>
438+
_jsxSorted(
439+
'test',
440+
{},
441+
null,
442+
[_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, '2')],
443+
0,
444+
'KA_6'
445+
)
449446
);
450-
const b1 = document.querySelector('b[key=1]')!;
451-
const b2 = document.querySelector('b[key=1]')!;
447+
const test = _jsxSorted(
448+
'test',
449+
{},
450+
null,
451+
[
452+
_jsxSorted('b', {}, null, 'before', 0, null),
453+
_jsxSorted('b', {}, null, '2', 0, '2'),
454+
_jsxSorted('b', {}, null, '3', 0, '3'),
455+
_jsxSorted('b', {}, null, 'in', 0, null),
456+
_jsxSorted('b', {}, null, '1', 0, '1'),
457+
_jsxSorted('b', {}, null, 'after', 0, null),
458+
],
459+
0,
460+
'KA_6'
461+
);
462+
const selectB1 = () => document.querySelector('b[q\\:key=1]')!;
463+
const selectB2 = () => document.querySelector('b[q\\:key=2]')!;
464+
const b1 = selectB1();
465+
const b2 = selectB2();
466+
expect(b1).toBeDefined();
467+
expect(b2).toBeDefined();
452468
vnode_diff({ $journal$: journal, document } as any, test, vParent, null);
453469
vnode_applyJournal(journal);
454470
expect(vNode).toMatchVDOM(test);
455-
expect(b1).toBe(document.querySelector('b[key=1]')!);
456-
expect(b2).toBe(document.querySelector('b[key=2]')!);
471+
expect(b1).toBe(selectB1());
472+
expect(b2).toBe(selectB2());
457473
});
458474
});
459475
describe.todo('fragments', () => {});

packages/qwik/src/testing/vdom-diff.unit-util.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,6 @@ export function vnode_fromJSX(jsx: JSXOutput) {
440440
const child = vnode_newUnMaterializedElement(doc.createElement(type));
441441
vnode_insertBefore(journal, vParent, child, null);
442442

443-
// TODO(hack): jsx.props is an empty object
444443
const props = jsx.varProps;
445444
for (const key in props) {
446445
if (Object.prototype.hasOwnProperty.call(props, key)) {

0 commit comments

Comments
 (0)