Skip to content

Commit 9449a5b

Browse files
committed
chore: add flags to numeric prop key
1 parent 3803e0c commit 9449a5b

File tree

9 files changed

+170
-56
lines changed

9 files changed

+170
-56
lines changed

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

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,14 @@ import {
2828
QSlotParent,
2929
QBackRefs,
3030
QTemplate,
31-
Q_PREFIX,
32-
dangerouslySetInnerHTML,
31+
HANDLER_PREFIX,
3332
} from '../shared/utils/markers';
3433
import { isPromise } from '../shared/utils/promises';
3534
import { type ValueOrPromise } from '../shared/utils/types';
3635
import {
3736
convertEventNameFromJsxPropToHtmlAttr,
3837
getEventNameFromJsxProp,
3938
getEventNameScopeFromJsxProp,
40-
isHtmlAttributeAnEventName,
41-
isJsxPropertyAnEventName,
4239
} from '../shared/utils/event-names';
4340
import { ChoreType } from '../shared/util-chore-type';
4441
import { hasClassAttr } from '../shared/utils/scoped-styles';
@@ -92,9 +89,13 @@ import { EffectProperty, isSignal, SubscriptionData } from '../signal/signal';
9289
import type { Signal } from '../signal/signal.public';
9390
import { executeComponent } from '../shared/component-execution';
9491
import {
92+
StaticPropId,
9593
getPropId,
9694
getPropName,
9795
getSlotName,
96+
isEventProp,
97+
isHandlerProp,
98+
isQProp,
9899
isSlotProp,
99100
type NumericPropKey,
100101
} from '../shared/utils/prop';
@@ -618,9 +619,9 @@ export const vnode_diff = (
618619
// We never tell the vNode about them saving us time and memory.
619620
for (const key in constProps) {
620621
let value = constProps[key as unknown as NumericPropKey];
621-
const numericKey = key as unknown as NumericPropKey;
622+
const numericKey = Number(key) as NumericPropKey;
622623
const nameKey = getPropName(numericKey);
623-
if (isJsxPropertyAnEventName(nameKey)) {
624+
if (isEventProp(numericKey)) {
624625
// So for event handlers we must add them to the vNode so that qwikloader can look them up
625626
// But we need to mark them so that they don't get pulled into the diff.
626627
const eventName = getEventNameFromJsxProp(nameKey);
@@ -648,7 +649,7 @@ export const vnode_diff = (
648649
continue;
649650
}
650651

651-
if (nameKey === 'ref') {
652+
if (numericKey === StaticPropId.REF) {
652653
if (isSignal(value)) {
653654
value.value = element;
654655
continue;
@@ -676,13 +677,13 @@ export const vnode_diff = (
676677
);
677678
}
678679

679-
if (nameKey === dangerouslySetInnerHTML) {
680+
if (numericKey === StaticPropId.INNERHTML) {
680681
element.innerHTML = value as string;
681682
element.setAttribute(QContainerAttr, QContainerValue.HTML);
682683
continue;
683684
}
684685

685-
if (elementName === 'textarea' && nameKey === 'value') {
686+
if (elementName === 'textarea' && numericKey === StaticPropId.VALUE) {
686687
if (value && typeof value !== 'string') {
687688
if (isDev) {
688689
throw qError(QError.wrongTextareaValue, [currentFile, value]);
@@ -764,11 +765,11 @@ export const vnode_diff = (
764765
for (const key in props) {
765766
const value = props[key as unknown as NumericPropKey];
766767
if (value != null) {
767-
mapArray_set(jsxAttrs, key as unknown as NumericPropKey, value, 0);
768+
mapArray_set(jsxAttrs, Number(key) as NumericPropKey, value, 0);
768769
}
769770
}
770771
if (jsxKey !== null) {
771-
mapArray_set(jsxAttrs, getPropId(ELEMENT_KEY), jsxKey, 0);
772+
mapArray_set(jsxAttrs, StaticPropId.ELEMENT_KEY as NumericPropKey, jsxKey, 0);
772773
}
773774
const vNode = (vNewNode || vCurrent) as ElementVNode;
774775
needsQDispatchEventPatch =
@@ -820,12 +821,12 @@ export const vnode_diff = (
820821

821822
const record = (key: NumericPropKey, value: any) => {
822823
const keyName = getPropName(key);
823-
if (keyName.startsWith(':')) {
824+
if (isHandlerProp(key)) {
824825
vnode_setProp(vnode, keyName, value);
825826
return;
826827
}
827828

828-
if (keyName === 'ref') {
829+
if (key === StaticPropId.REF) {
829830
const element = vnode_getNode(vnode) as Element;
830831
if (isSignal(value)) {
831832
value.value = element;
@@ -882,17 +883,14 @@ export const vnode_diff = (
882883
};
883884

884885
while (srcKey !== null || dstKey !== null) {
885-
if (
886-
(dstKey && getPropName(dstKey).startsWith(HANDLER_PREFIX)) ||
887-
(dstKey && getPropName(dstKey).startsWith(Q_PREFIX))
888-
) {
886+
if ((dstKey && isHandlerProp(dstKey)) || (dstKey && isQProp(dstKey))) {
889887
// These are a special keys which we use to mark the event handlers as immutable or
890888
// element key we need to ignore them.
891889
dstIdx++; // skip the destination value, we don't care about it.
892890
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
893891
} else if (srcKey == null) {
894892
// Source has more keys, so we need to remove them from destination
895-
if (dstKey && isHtmlAttributeAnEventName(getPropName(dstKey))) {
893+
if (dstKey && isEventProp(dstKey)) {
896894
patchEventDispatch = true;
897895
dstIdx++;
898896
} else {
@@ -902,7 +900,7 @@ export const vnode_diff = (
902900
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
903901
} else if (dstKey == null) {
904902
// Destination has more keys, so we need to insert them from source.
905-
const isEvent = isJsxPropertyAnEventName(getPropName(srcKey));
903+
const isEvent = isEventProp(srcKey);
906904
if (isEvent) {
907905
// Special handling for events
908906
patchEventDispatch = true;
@@ -926,7 +924,7 @@ export const vnode_diff = (
926924
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
927925
} else if (srcKey < dstKey) {
928926
// Destination is missing the key, so we need to insert it.
929-
if (isJsxPropertyAnEventName(getPropName(srcKey))) {
927+
if (isEventProp(srcKey)) {
930928
// Special handling for events
931929
patchEventDispatch = true;
932930
recordJsxEvent(srcKey, srcAttrs[srcIdx]);
@@ -943,7 +941,7 @@ export const vnode_diff = (
943941
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
944942
} else {
945943
// Source is missing the key, so we need to remove it from destination.
946-
if (isHtmlAttributeAnEventName(getPropName(dstKey))) {
944+
if (isEventProp(dstKey)) {
947945
patchEventDispatch = true;
948946
dstIdx++;
949947
} else {
@@ -1219,7 +1217,15 @@ function getKey(vNode: VNode | null): string | null {
12191217
if (vNode == null) {
12201218
return null;
12211219
}
1222-
return vnode_getProp<string>(vNode, ELEMENT_KEY, null);
1220+
const type = vNode[VNodeProps.flags];
1221+
if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) {
1222+
const props = vnode_getProps(vNode);
1223+
// this works, because q:key is always at 0 position or it is not present
1224+
if (props[0] === StaticPropId.ELEMENT_KEY) {
1225+
return props[1] as string | null;
1226+
}
1227+
}
1228+
return null;
12231229
}
12241230

12251231
/**
@@ -1450,11 +1456,6 @@ function markVNodeAsDeleted(vCursor: VNode) {
14501456
vCursor[VNodeProps.flags] |= VNodeFlags.Deleted;
14511457
}
14521458

1453-
/**
1454-
* This marks the property as immutable. It is needed for the QRLs so that QwikLoader can get a hold
1455-
* of them. This character must be `:` so that the `vnode_getAttr` can ignore them.
1456-
*/
1457-
const HANDLER_PREFIX = ':';
14581459
let count = 0;
14591460
const enum SiblingsArray {
14601461
Name = 0,

packages/qwik/src/core/shared/utils/event-names.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const convertEventNameFromHtmlAttrToJsxProp = (name: string): string | nu
125125
return null;
126126
};
127127

128+
const DOMContentLoadedEventLowercase = DOMContentLoadedEvent.toLowerCase();
128129
export const createEventName = (
129130
event: KnownEventNames | KnownEventNames[],
130131
eventType?: 'window' | 'document' | undefined
@@ -133,7 +134,7 @@ export const createEventName = (
133134
const map = (name: string) => {
134135
// DOMContentLoaded is a special case, where the event name is case sensitive
135136
// https://html.spec.whatwg.org/multipage/indices.html#event-domcontentloaded
136-
const isDOMContentLoadedEvent = name === DOMContentLoadedEvent;
137+
const isDOMContentLoadedEvent = name.toLowerCase() === DOMContentLoadedEventLowercase;
137138
const eventName = isDOMContentLoadedEvent
138139
? DOMContentLoadedEvent
139140
: name.charAt(0).toUpperCase() + name.substring(1).toLowerCase();

packages/qwik/src/core/shared/utils/markers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ export const ELEMENT_SEQ = 'q:seq';
7878
export const ELEMENT_SEQ_IDX = 'q:seqIdx';
7979
export const Q_PREFIX = 'q:';
8080

81+
/**
82+
* This marks the property as immutable. It is needed for the QRLs so that QwikLoader can get a hold
83+
* of them. This character must be `:` so that the `vnode_getAttr` can ignore them.
84+
*/
85+
export const HANDLER_PREFIX = ':';
86+
8187
/** Non serializable markers - always begins with `:` character */
8288
export const NON_SERIALIZABLE_MARKER_PREFIX = ':';
8389
export const USE_ON_LOCAL = NON_SERIALIZABLE_MARKER_PREFIX + 'on';
@@ -92,5 +98,6 @@ export const STREAM_BLOCK_END_COMMENT = 'qkssr-po';
9298
export const Q_PROPS_SEPARATOR = ':';
9399

94100
export const dangerouslySetInnerHTML = 'dangerouslySetInnerHTML';
101+
export const refAttr = 'ref';
95102
export const qwikInspectorAttr = 'data-qwik-inspector';
96103
export const DOMContentLoadedEvent = 'DOMContentLoaded';

packages/qwik/src/core/shared/utils/prop.ts

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,84 @@ import type { JSXNodeInternal } from '../jsx/types/jsx-node';
1010
import type { KnownEventNames } from '../jsx/types/jsx-qwik-events';
1111
import type { Container, HostElement } from '../types';
1212
import { _CONST_PROPS, _VAR_PROPS } from './constants';
13-
import { createEventName, parseEventNameFromIndex, isJsxPropertyAnEventName } from './event-names';
14-
import { NON_SERIALIZABLE_MARKER_PREFIX, QDefaultSlot, Q_PREFIX } from './markers';
13+
import {
14+
createEventName,
15+
parseEventNameFromIndex,
16+
isJsxPropertyAnEventName,
17+
isHtmlAttributeAnEventName,
18+
} from './event-names';
19+
import {
20+
ELEMENT_ID,
21+
ELEMENT_KEY,
22+
ELEMENT_PROPS,
23+
HANDLER_PREFIX,
24+
NON_SERIALIZABLE_MARKER_PREFIX,
25+
OnRenderProp,
26+
QDefaultSlot,
27+
Q_PREFIX,
28+
dangerouslySetInnerHTML,
29+
refAttr,
30+
} from './markers';
1531

1632
const propNameToId = new Map<string | symbol, NumericPropKey>();
17-
export const idToPropName: (string | symbol)[] = [null!];
33+
const idToPropName: (string | symbol)[] = [];
1834
export type NumericPropKey = number & { __brand__: 'NumericPropKey' };
1935

2036
const colonOnLength = ':on'.length;
2137

38+
export const enum NumericPropKeyFlags {
39+
EVENT = 1,
40+
Q_PREFIX = 2,
41+
HANDLER_PREFIX = 4,
42+
SLOT = 8,
43+
}
44+
45+
export const NumericFlagsShift = 4;
46+
2247
export const getPropId = (name: string | symbol): NumericPropKey => {
2348
let id = propNameToId.get(name);
24-
if (id) {
49+
if (id != null) {
2550
return id;
2651
}
27-
id = idToPropName.length as NumericPropKey;
28-
if (typeof name === 'string' && isJsxPropertyAnEventName(name)) {
29-
name = normalizeEvent(name);
52+
id = (idToPropName.length << NumericFlagsShift) as NumericPropKey;
53+
if (typeof name === 'string') {
54+
if (isJsxPropertyAnEventName(name)) {
55+
name = normalizeEvent(name);
56+
(id as number) |= NumericPropKeyFlags.EVENT;
57+
} else if (isHtmlAttributeAnEventName(name)) {
58+
(id as number) |= NumericPropKeyFlags.EVENT;
59+
} else if (name.startsWith(Q_PREFIX)) {
60+
(id as number) |= NumericPropKeyFlags.Q_PREFIX;
61+
} else if (name.startsWith(HANDLER_PREFIX)) {
62+
(id as number) |= NumericPropKeyFlags.HANDLER_PREFIX;
63+
}
64+
65+
if (!name.startsWith(Q_PREFIX) && !name.startsWith(NON_SERIALIZABLE_MARKER_PREFIX)) {
66+
(id as number) |= NumericPropKeyFlags.SLOT;
67+
}
3068
}
3169
idToPropName.push(name);
3270
propNameToId.set(name, id);
3371
return id;
3472
};
3573

74+
export const StaticPropId = {
75+
// ELEMENT_KEY should be always first, because of `getKey` in vnode_diff.ts
76+
ELEMENT_KEY: getPropId(ELEMENT_KEY),
77+
ELEMENT_ID: getPropId(ELEMENT_ID),
78+
ELEMENT_PROPS: getPropId(ELEMENT_PROPS),
79+
REF: getPropId(refAttr),
80+
INNERHTML: getPropId(dangerouslySetInnerHTML),
81+
VALUE: getPropId('value'),
82+
ON_RENDER: getPropId(OnRenderProp),
83+
CLASS: getPropId('class'),
84+
CLASS_NAME: getPropId('classname'),
85+
};
86+
87+
export const getPropName = <T extends string>(id: NumericPropKey): T => {
88+
return idToPropName[id >> NumericFlagsShift] as T;
89+
};
90+
3691
function normalizeEvent(name: string): string {
3792
const index = name.indexOf(':on');
3893
const scope = (name.substring(0, index) || undefined) as 'window' | 'document' | undefined;
@@ -41,13 +96,24 @@ function normalizeEvent(name: string): string {
4196
return name;
4297
}
4398

44-
export const getPropName = <T extends string>(id: NumericPropKey): T => {
45-
return idToPropName[id] as T;
46-
};
99+
function getFlags(id: number) {
100+
return ((1 << NumericFlagsShift) - 1) & (id >> 0);
101+
}
102+
103+
export function isEventProp(numericProp: NumericPropKey): boolean {
104+
return (getFlags(numericProp) & NumericPropKeyFlags.EVENT) !== 0;
105+
}
106+
107+
export function isQProp(numericProp: NumericPropKey): boolean {
108+
return (getFlags(numericProp) & NumericPropKeyFlags.Q_PREFIX) !== 0;
109+
}
110+
111+
export function isHandlerProp(numericProp: NumericPropKey): boolean {
112+
return (getFlags(numericProp) & NumericPropKeyFlags.HANDLER_PREFIX) !== 0;
113+
}
47114

48115
export function isSlotProp(numericProp: NumericPropKey): boolean {
49-
const prop = idToPropName[numericProp] as string;
50-
return !prop.startsWith(Q_PREFIX) && !prop.startsWith(NON_SERIALIZABLE_MARKER_PREFIX);
116+
return (getFlags(numericProp) & NumericPropKeyFlags.SLOT) !== 0;
51117
}
52118

53119
export function getSlotName(
@@ -88,3 +154,8 @@ export const _restProps = (props: PropsProxy, omit: string[], target: Props = {}
88154

89155
return createPropsProxy(varPropsTarget, constPropsTarget);
90156
};
157+
158+
export const __testing__ = {
159+
propNameToId,
160+
idToPropName,
161+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import * as propFile from './prop';
3+
4+
describe('prop', () => {
5+
beforeEach(() => {
6+
propFile.__testing__.propNameToId.clear();
7+
propFile.__testing__.idToPropName.length = 0;
8+
});
9+
10+
describe('getPropId', () => {
11+
it('adding props to map', async () => {
12+
expect(propFile.getPropName(propFile.getPropId('firstProp'))).toBe('firstProp');
13+
expect(propFile.getPropName(propFile.getPropId('secondProp'))).toBe('secondProp');
14+
});
15+
16+
it('adding event', async () => {
17+
expect(propFile.getPropName(propFile.getPropId('onClick$'))).toBe('onClick$');
18+
expect(propFile.getPropName(propFile.getPropId('window:onClick$'))).toBe('window:onClick$');
19+
expect(propFile.getPropName(propFile.getPropId('document:onClick$'))).toBe(
20+
'document:onClick$'
21+
);
22+
expect(propFile.getPropName(propFile.getPropId('onDblClick$'))).toBe('onDblclick$');
23+
expect(propFile.getPropName(propFile.getPropId('window:onDblClick$'))).toBe(
24+
'window:onDblclick$'
25+
);
26+
expect(propFile.getPropName(propFile.getPropId('document:onDblClick$'))).toBe(
27+
'document:onDblclick$'
28+
);
29+
expect(propFile.getPropName(propFile.getPropId('document:onDOMContentLoaded$'))).toBe(
30+
'document:onDOMContentLoaded$'
31+
);
32+
});
33+
});
34+
});

0 commit comments

Comments
 (0)