Skip to content

Commit

Permalink
serialize virtual props for DOM elements
Browse files Browse the repository at this point in the history
  • Loading branch information
Varixo committed Dec 10, 2024
1 parent 34abc18 commit 72d7c24
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 157 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-berries-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: serialize virtual props for DOM elements
20 changes: 13 additions & 7 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import {
QSlot,
QSlotParent,
QStyle,
QSubscribers,
QTemplate,
Q_PREFIX,
dangerouslySetInnerHTML,
} from '../shared/utils/markers';
import { isPromise } from '../shared/utils/promises';
Expand Down Expand Up @@ -817,7 +819,7 @@ export const vnode_diff = (
};

while (srcKey !== null || dstKey !== null) {
if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey == ELEMENT_KEY) {
if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey?.startsWith(Q_PREFIX)) {
// These are a special keys which we use to mark the event handlers as immutable or
// element key we need to ignore them.
dstIdx++; // skip the destination value, we don't care about it.
Expand Down Expand Up @@ -1203,8 +1205,8 @@ function propsDiffer(src: Record<string, any>, dst: Record<string, any>): boolea
if (!src || !dst) {
return true;
}
let srcKeys = removeChildrenKey(Object.keys(src));
let dstKeys = removeChildrenKey(Object.keys(dst));
let srcKeys = removePropsKeys(Object.keys(src), ['children', QSubscribers]);
let dstKeys = removePropsKeys(Object.keys(dst), ['children', QSubscribers]);
if (srcKeys.length !== dstKeys.length) {
return true;
}
Expand All @@ -1220,11 +1222,15 @@ function propsDiffer(src: Record<string, any>, dst: Record<string, any>): boolea
return false;
}

function removeChildrenKey(keys: string[]): string[] {
const childrenIdx = keys.indexOf('children');
if (childrenIdx !== -1) {
keys.splice(childrenIdx, 1);
function removePropsKeys(keys: string[], propKeys: string[]): string[] {
for (let i = propKeys.length - 1; i >= 0; i--) {
const propKey = propKeys[i];
const propIdx = keys.indexOf(propKey);
if (propIdx !== -1) {
keys.splice(propIdx, 1);
}
}

return keys;
}

Expand Down
163 changes: 122 additions & 41 deletions packages/qwik/src/core/client/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,12 +1229,57 @@ export const vnode_materialize = (vNode: ElementVNode) => {
const element = vNode[ElementVNodeProps.element];
const firstChild = fastFirstChild(element);
const vNodeData = (element.ownerDocument as QDocument)?.qVNodeData?.get(element);
const vFirstChild = vNodeData
? materializeFromVNodeData(vNode, vNodeData, element, firstChild)
: materializeFromDOM(vNode, firstChild);

const vFirstChild = materialize(vNode, element, firstChild, vNodeData);
return vFirstChild;
};

const materialize = (
vNode: ElementVNode,
element: Element,
firstChild: Node | null,
vNodeData?: string
): VNode | null => {
if (vNodeData) {
if (vNodeData.charCodeAt(0) === VNodeDataChar.SEPARATOR) {
/**
* If vNodeData start with the `VNodeDataChar.SEPARATOR` then it means that the vNodeData
* contains some data for DOM element. We need to split it to DOM element vNodeData and
* virtual element vNodeData.
*
* For example `|=6`4|2{J=7`3|q:type|S}` should split into `=6`4`and`2{J=7`3|q:type|S}`, where
* `=6`4` is vNodeData for the DOM element.
*/

const elementVNodeDataStartIdx = 1;
let elementVNodeDataEndIdx = 1;
while (vNodeData.charCodeAt(elementVNodeDataEndIdx) !== VNodeDataChar.SEPARATOR) {
elementVNodeDataEndIdx++;
}
const elementVNodeData = vNodeData.substring(
elementVNodeDataStartIdx,
elementVNodeDataEndIdx
);

// Override vNodeData variable for materializing a virtual element
vNodeData = vNodeData.substring(elementVNodeDataEndIdx + 1);

// Materialize DOM element from HTML. If the `vNodeData` is not empty,
// then also materialize virtual element from vNodeData
const vFirstChild = materializeFromDOM(vNode, firstChild, elementVNodeData);
if (!vNodeData) {
// If it is empty then we don't need to call the `materializeFromVNodeData`.
return vFirstChild;
}
}
// Materialize virtual element form vNodeData
return materializeFromVNodeData(vNode, vNodeData, element, firstChild);
} else {
// Materialize DOM element from HTML only
return materializeFromDOM(vNode, firstChild);
}
};

const ensureMaterialized = (vnode: ElementVNode): VNode | null => {
const vParent = ensureElementVNode(vnode);
let vFirstChild = vParent[ElementVNodeProps.firstChild];
Expand Down Expand Up @@ -1381,7 +1426,7 @@ const isQStyleElement = (node: Node | null): node is Element => {
);
};

const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => {
const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null, vData?: string) => {
let vFirstChild: VNode | null = null;

const skipStyleElements = () => {
Expand Down Expand Up @@ -1417,9 +1462,78 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => {
}
vParent[ElementVNodeProps.lastChild] = vChild || null;
vParent[ElementVNodeProps.firstChild] = vFirstChild;

if (vData) {
/**
* If we need to materialize from DOM and we have vNodeData it means that we have some virtual
* props for that node.
*/
let container: ClientContainer | null = null;
processVNodeData(vData, (peek, consumeValue) => {
if (peek() === VNodeDataChar.ID) {
if (!container) {
container = getDomContainer(vParent[ElementVNodeProps.element]);
}
const id = consumeValue();
container.$setRawState$(parseInt(id), vParent);
isDev && vnode_setAttr(null, vParent, ELEMENT_ID, id);
} else if (peek() === VNodeDataChar.SUBS) {
vnode_setProp(vParent, QSubscribers, consumeValue());
} else {
// prevent infinity loop if there are some characters outside the range
consumeValue();
}
});
}

return vFirstChild;
};

const processVNodeData = (
vData: string,
callback: (
peek: () => number,
consumeValue: () => string,
consume: () => number,
nextToConsumeIdx: number
) => void
) => {
let nextToConsumeIdx = 0;
let ch = 0;
let peekCh = 0;
const peek = () => {
if (peekCh !== 0) {
return peekCh;
} else {
return (peekCh = nextToConsumeIdx < vData.length ? vData.charCodeAt(nextToConsumeIdx) : 0);
}
};
const consume = () => {
ch = peek();
peekCh = 0;
nextToConsumeIdx++;
return ch;
};

const consumeValue = () => {
consume();
const start = nextToConsumeIdx;
while (
(peek() <= 58 /* `:` */ && peekCh !== 0) ||
peekCh === 95 /* `_` */ ||
(peekCh >= 65 /* `A` */ && peekCh <= 90) /* `Z` */ ||
(peekCh >= 97 /* `a` */ && peekCh <= 122) /* `z` */
) {
consume();
}
return vData.substring(start, nextToConsumeIdx);
};

while (peek() !== 0) {
callback(peek, consumeValue, consume, nextToConsumeIdx);
}
};

export const vnode_getNextSibling = (vnode: VNode): VNode | null => {
return vnode[VNodeProps.nextSibling];
};
Expand Down Expand Up @@ -1639,25 +1753,10 @@ function materializeFromVNodeData(
child: Node | null
): VNode {
let idx = 0;
let nextToConsumeIdx = 0;
let vFirst: VNode | null = null;
let vLast: VNode | null = null;
let previousTextNode: TextVNode | null = null;
let ch = 0;
let peekCh = 0;
const peek = () => {
if (peekCh !== 0) {
return peekCh;
} else {
return (peekCh = nextToConsumeIdx < vData!.length ? vData!.charCodeAt(nextToConsumeIdx) : 0);
}
};
const consume = () => {
ch = peek();
peekCh = 0;
nextToConsumeIdx++;
return ch;
};

const addVNode = (node: VNode) => {
node[VNodeProps.flags] =
(node[VNodeProps.flags] & VNodeFlagsIndex.negated_mask) | (idx << VNodeFlagsIndex.shift);
Expand All @@ -1671,29 +1770,11 @@ function materializeFromVNodeData(
vLast = node;
};

const consumeValue = () => {
consume();
const start = nextToConsumeIdx;
while (
(peek() <= 58 /* `:` */ && peekCh !== 0) ||
peekCh === 95 /* `_` */ ||
(peekCh >= 65 /* `A` */ && peekCh <= 90) /* `Z` */ ||
(peekCh >= 97 /* `a` */ && peekCh <= 122) /* `z` */
) {
consume();
}
return vData.substring(start, nextToConsumeIdx);
};

let textIdx = 0;
let combinedText: string | null = null;
let container: ClientContainer | null = null;
// console.log(
// 'processVNodeData',
// vNodeData,
// (child?.parentNode as HTMLElement | undefined)?.outerHTML
// );
while (peek() !== 0) {

processVNodeData(vData, (peek, consumeValue, consume, nextToConsumeIdx) => {
if (isNumber(peek())) {
// Element counts get encoded as numbers.
while (!isElement(child)) {
Expand Down Expand Up @@ -1790,7 +1871,7 @@ function materializeFromVNodeData(
textIdx += length;
// Text nodes get encoded as alphanumeric characters.
}
}
});
vParent[ElementVNodeProps.lastChild] = vLast;
return vFirst!;
}
Expand Down
34 changes: 17 additions & 17 deletions packages/qwik/src/core/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { _CONST_PROPS, _VAR_PROPS } from './utils/constants';
import { isElement, isNode } from './utils/element';
import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight';
import { throwErrorAndStop } from './utils/log';
import { ELEMENT_ID } from './utils/markers';
import { ELEMENT_ID, ELEMENT_KEY } from './utils/markers';
import { isPromise } from './utils/promises';
import { fastSkipSerialize } from './utils/serialize-utils';
import { type ValueOrPromise } from './utils/types';
Expand Down Expand Up @@ -843,25 +843,11 @@ export const createSerializationContext = (
} else if (obj instanceof Task) {
discoveredValues.push(obj.$el$, obj.$qrl$, obj.$state$, obj.$effectDependencies$);
} else if (isSsrNode(obj)) {
for (const value of obj.vnodeData) {
if (isSsrAttrs(value)) {
for (let i = 1; i < value.length; i += 2) {
const attrValue = value[i];
discoveredValues.push(attrValue);
}
}
}
discoverValuesForVNodeData(obj.vnodeData, discoveredValues);

if (obj.childrenVNodeData && obj.childrenVNodeData.length) {
for (const data of obj.childrenVNodeData) {
for (const value of data) {
if (isSsrAttrs(value)) {
for (let i = 1; i < value.length; i += 2) {
const attrValue = value[i];
discoveredValues.push(attrValue);
}
}
}
discoverValuesForVNodeData(data, discoveredValues);
}
}
} else if (isDomRef(obj)) {
Expand Down Expand Up @@ -929,6 +915,20 @@ export const createSerializationContext = (
const isSsrAttrs = (value: number | SsrAttrs): value is SsrAttrs =>
Array.isArray(value) && value.length > 0;

const discoverValuesForVNodeData = (vnodeData: VNodeData, discoveredValues: unknown[]) => {
for (const value of vnodeData) {
if (isSsrAttrs(value)) {
for (let i = 1; i < value.length; i += 2) {
if (value[i - 1] === ELEMENT_KEY) {
continue;
}
const attrValue = value[i];
discoveredValues.push(attrValue);
}
}
}
};

const promiseResults = new WeakMap<Promise<any>, [boolean, unknown]>();

/**
Expand Down
1 change: 1 addition & 0 deletions packages/qwik/src/core/shared/utils/markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const ELEMENT_SELF_ID = -1;
export const ELEMENT_ID_SELECTOR = '[q\\:id]';
export const ELEMENT_ID_PREFIX = '#';
export const INLINE_FN_PREFIX = '@';
export const Q_PREFIX = 'q:';

/** Non serializable markers - always begins with `:` character */
export const NON_SERIALIZABLE_MARKER_PREFIX = ':';
Expand Down
10 changes: 7 additions & 3 deletions packages/qwik/src/core/shared/vnode-data-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ export const VNodeDataSeparator = {
ADVANCE_8192: /* ****** */ 46, // `.` is vNodeData separator skipping 4096.
};

/** VNodeDataChar contains information about the VNodeData used for encoding props */
/**
* VNodeDataChar contains information about the VNodeData used for encoding props.
*
* Available character ranges: 59 - 64, 91 - 94, 96, 123 - 126
*/
export const VNodeDataChar = {
OPEN: /* ************** */ 123, // `{` is the start of the VNodeData.
OPEN: /* ************** */ 123, // `{` is the start of the VNodeData for a virtual element.
OPEN_CHAR: /* ****** */ '{',
CLOSE: /* ************* */ 125, // `}` is the end of the VNodeData.
CLOSE: /* ************* */ 125, // `}` is the end of the VNodeData for a virtual element.
CLOSE_CHAR: /* ***** */ '}',

SCOPED_STYLE: /* ******* */ 59, // `;` - `q:sstyle` - Style attribute.
Expand Down
Loading

0 comments on commit 72d7c24

Please sign in to comment.