Skip to content

Commit 4794f2a

Browse files
committed
fix: adding and removing attributes on vnodes
1 parent b1d99a6 commit 4794f2a

File tree

3 files changed

+374
-58
lines changed

3 files changed

+374
-58
lines changed

.changeset/shiny-readers-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: adding and removing attributes on vnodes

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

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import { hasClassAttr } from '../shared/utils/scoped-styles';
4545
import type { HostElement, QElement, QwikLoaderEventScope, qWindow } from '../shared/types';
4646
import { DEBUG_TYPE, QContainerValue, VirtualType } from '../shared/types';
4747
import type { DomContainer } from './dom-container';
48-
import { VNodeFlags, type ClientAttrKey, type ClientAttrs, type ClientContainer } from './types';
48+
import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types';
4949
import {
5050
vnode_ensureElementInflated,
5151
vnode_getDomParentVNode,
@@ -783,11 +783,7 @@ export const vnode_diff = (
783783
vnode_ensureElementInflated(vnode);
784784
const dstAttrs = vnode_getProps(vnode) as ClientAttrs;
785785
let srcIdx = 0;
786-
const srcLength = srcAttrs.length;
787786
let dstIdx = 0;
788-
let dstLength = dstAttrs.length;
789-
let srcKey: ClientAttrKey | null = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
790-
let dstKey: ClientAttrKey | null = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
791787
let patchEventDispatch = false;
792788

793789
const record = (key: string, value: any) => {
@@ -820,10 +816,6 @@ export const vnode_diff = (
820816
value !== null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null,
821817
journal
822818
);
823-
if (value === null) {
824-
// if we set `null` than attribute was removed and we need to shorten the dstLength
825-
dstLength = dstAttrs.length;
826-
}
827819
};
828820

829821
const recordJsxEvent = (key: string, value: any) => {
@@ -846,73 +838,70 @@ export const vnode_diff = (
846838
}
847839
};
848840

849-
while (srcKey !== null || dstKey !== null) {
841+
// Two-pointer merge algorithm: both arrays are sorted by key
842+
// Note: dstAttrs mutates during iteration (setAttr uses splice), so we re-read keys each iteration
843+
while (srcIdx < srcAttrs.length || dstIdx < dstAttrs.length) {
844+
const srcKey = srcIdx < srcAttrs.length ? (srcAttrs[srcIdx] as string) : undefined;
845+
const dstKey = dstIdx < dstAttrs.length ? (dstAttrs[dstIdx] as string) : undefined;
846+
847+
// Skip special keys in destination (HANDLER_PREFIX, Q_PREFIX)
850848
if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey?.startsWith(Q_PREFIX)) {
851-
// These are a special keys which we use to mark the event handlers as immutable or
852-
// element key we need to ignore them.
853-
dstIdx++; // skip the destination value, we don't care about it.
854-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
855-
} else if (srcKey == null) {
856-
// Source has more keys, so we need to remove them from destination
857-
if (dstKey && isHtmlAttributeAnEventName(dstKey)) {
858-
dstIdx++;
849+
dstIdx += 2; // skip key and value
850+
continue;
851+
}
852+
853+
if (srcKey === undefined) {
854+
// Source exhausted: remove remaining destination keys
855+
if (isHtmlAttributeAnEventName(dstKey!)) {
856+
// HTML event attributes are immutable and not removed from DOM
857+
dstIdx += 2; // skip key and value
859858
} else {
860859
record(dstKey!, null);
861-
dstIdx--;
860+
// After removal, dstAttrs shrinks by 2, so don't advance dstIdx
862861
}
863-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
864-
} else if (dstKey == null) {
865-
// Destination has more keys, so we need to insert them from source.
866-
const isEvent = isJsxPropertyAnEventName(srcKey);
867-
if (isEvent) {
868-
// Special handling for events
862+
} else if (dstKey === undefined) {
863+
// Destination exhausted: add remaining source keys
864+
const srcValue = srcAttrs[srcIdx + 1];
865+
if (isJsxPropertyAnEventName(srcKey)) {
869866
patchEventDispatch = true;
870-
recordJsxEvent(srcKey, srcAttrs[srcIdx]);
867+
recordJsxEvent(srcKey, srcValue);
871868
} else {
872-
record(srcKey!, srcAttrs[srcIdx]);
869+
record(srcKey, srcValue);
873870
}
874-
srcIdx++;
875-
srcKey = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
876-
// we need to increment dstIdx too, because we added destination key and value to the VNode
877-
// and dstAttrs is a reference to the VNode
878-
dstIdx++;
879-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
880-
} else if (srcKey == dstKey) {
881-
const srcValue = srcAttrs[srcIdx++];
882-
const dstValue = dstAttrs[dstIdx++];
871+
srcIdx += 2; // skip key and value
872+
// After addition, dstAttrs grows by 2 at sorted position, advance dstIdx
873+
dstIdx += 2;
874+
} else if (srcKey === dstKey) {
875+
// Keys match: update if values differ
876+
const srcValue = srcAttrs[srcIdx + 1];
877+
const dstValue = dstAttrs[dstIdx + 1];
883878
if (srcValue !== dstValue) {
884-
record(dstKey, srcValue);
879+
record(srcKey, srcValue);
880+
// Update in place doesn't change array length
885881
}
886-
srcKey = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
887-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
882+
srcIdx += 2; // skip key and value
883+
dstIdx += 2; // skip key and value
888884
} else if (srcKey < dstKey) {
889-
// Destination is missing the key, so we need to insert it.
885+
// Source has a key not in destination: add it
886+
const srcValue = srcAttrs[srcIdx + 1];
890887
if (isJsxPropertyAnEventName(srcKey)) {
891-
// Special handling for events
892888
patchEventDispatch = true;
893-
recordJsxEvent(srcKey, srcAttrs[srcIdx]);
889+
recordJsxEvent(srcKey, srcValue);
894890
} else {
895-
record(srcKey, srcAttrs[srcIdx]);
891+
record(srcKey, srcValue);
896892
}
897-
898-
srcIdx++;
899-
// advance srcValue
900-
srcKey = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
901-
// we need to increment dstIdx too, because we added destination key and value to the VNode
902-
// and dstAttrs is a reference to the VNode
903-
dstIdx++;
904-
dstLength = dstAttrs.length;
905-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
893+
srcIdx += 2; // skip key and value
894+
// After addition, dstAttrs grows at sorted position (before dstIdx), advance dstIdx
895+
dstIdx += 2;
906896
} else {
907-
// Source is missing the key, so we need to remove it from destination.
897+
// Destination has a key not in source: remove it (dstKey > srcKey)
908898
if (isHtmlAttributeAnEventName(dstKey)) {
909-
patchEventDispatch = true;
910-
dstIdx++;
899+
// HTML event attributes are immutable and not removed from DOM
900+
dstIdx += 2; // skip key and value
911901
} else {
912-
record(dstKey!, null);
913-
dstIdx--;
902+
record(dstKey, null);
903+
// After removal, dstAttrs shrinks at dstIdx, so don't advance dstIdx
914904
}
915-
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
916905
}
917906
}
918907
return patchEventDispatch;

0 commit comments

Comments
 (0)