@@ -45,7 +45,7 @@ import { hasClassAttr } from '../shared/utils/scoped-styles';
45
45
import type { HostElement , QElement , QwikLoaderEventScope , qWindow } from '../shared/types' ;
46
46
import { DEBUG_TYPE , QContainerValue , VirtualType } from '../shared/types' ;
47
47
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' ;
49
49
import {
50
50
vnode_ensureElementInflated ,
51
51
vnode_getDomParentVNode ,
@@ -783,11 +783,7 @@ export const vnode_diff = (
783
783
vnode_ensureElementInflated ( vnode ) ;
784
784
const dstAttrs = vnode_getProps ( vnode ) as ClientAttrs ;
785
785
let srcIdx = 0 ;
786
- const srcLength = srcAttrs . length ;
787
786
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 ;
791
787
let patchEventDispatch = false ;
792
788
793
789
const record = ( key : string , value : any ) => {
@@ -820,10 +816,6 @@ export const vnode_diff = (
820
816
value !== null ? serializeAttribute ( key , value , scopedStyleIdPrefix ) : null ,
821
817
journal
822
818
) ;
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
- }
827
819
} ;
828
820
829
821
const recordJsxEvent = ( key : string , value : any ) => {
@@ -846,73 +838,70 @@ export const vnode_diff = (
846
838
}
847
839
} ;
848
840
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)
850
848
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
859
858
} else {
860
859
record ( dstKey ! , null ) ;
861
- dstIdx -- ;
860
+ // After removal, dstAttrs shrinks by 2, so don't advance dstIdx
862
861
}
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 ) ) {
869
866
patchEventDispatch = true ;
870
- recordJsxEvent ( srcKey , srcAttrs [ srcIdx ] ) ;
867
+ recordJsxEvent ( srcKey , srcValue ) ;
871
868
} else {
872
- record ( srcKey ! , srcAttrs [ srcIdx ] ) ;
869
+ record ( srcKey , srcValue ) ;
873
870
}
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 ] ;
883
878
if ( srcValue !== dstValue ) {
884
- record ( dstKey , srcValue ) ;
879
+ record ( srcKey , srcValue ) ;
880
+ // Update in place doesn't change array length
885
881
}
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
888
884
} 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 ] ;
890
887
if ( isJsxPropertyAnEventName ( srcKey ) ) {
891
- // Special handling for events
892
888
patchEventDispatch = true ;
893
- recordJsxEvent ( srcKey , srcAttrs [ srcIdx ] ) ;
889
+ recordJsxEvent ( srcKey , srcValue ) ;
894
890
} else {
895
- record ( srcKey , srcAttrs [ srcIdx ] ) ;
891
+ record ( srcKey , srcValue ) ;
896
892
}
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 ;
906
896
} 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)
908
898
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
911
901
} else {
912
- record ( dstKey ! , null ) ;
913
- dstIdx -- ;
902
+ record ( dstKey , null ) ;
903
+ // After removal, dstAttrs shrinks at dstIdx, so don't advance dstIdx
914
904
}
915
- dstKey = dstIdx < dstLength ? dstAttrs [ dstIdx ++ ] : null ;
916
905
}
917
906
}
918
907
return patchEventDispatch ;
0 commit comments