diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 1f2d6624542..4bd33a56c0d 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -27,6 +27,7 @@ import { QScopedStyle, QStyle, QStyleSelector, + QStylesAllSelector, Q_PROPS_SEPARATOR, USE_ON_LOCAL_SEQ_IDX, getQFuncs, @@ -47,23 +48,22 @@ import { } from './types'; import { mapArray_get, mapArray_has, mapArray_set } from './util-mapArray'; import { - VNodeJournalOpCode, - vnode_applyJournal, vnode_createErrorDiv, - vnode_getDomParent, - vnode_getProps, + vnode_getProp, vnode_insertBefore, vnode_isElementVNode, - vnode_isVNode, vnode_isVirtualVNode, vnode_locate, vnode_newUnMaterializedElement, + vnode_setProp, type VNodeJournal, } from './vnode'; -import type { ElementVNode, VNode, VirtualVNode } from './vnode-impl'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; +import type { VNode } from '../shared/vnode/vnode'; +import type { VirtualVNode } from '../shared/vnode/virtual-vnode'; /** @public */ -export function getDomContainer(element: Element | VNode): IClientContainer { +export function getDomContainer(element: Element): IClientContainer { const qContainerElement = _getQContainerElement(element); if (!qContainerElement) { throw qError(QError.containerNotFound); @@ -73,19 +73,12 @@ export function getDomContainer(element: Element | VNode): IClientContainer { export function getDomContainerFromQContainerElement(qContainerElement: Element): IClientContainer { const qElement = qContainerElement as ContainerElement; - let container = qElement.qContainer; - if (!container) { - container = new DomContainer(qElement); - } - return container; + return (qElement.qContainer ||= new DomContainer(qElement)); } /** @internal */ -export function _getQContainerElement(element: Element | VNode): Element | null { - const qContainerElement: Element | null = vnode_isVNode(element) - ? (vnode_getDomParent(element, true) as Element) - : element; - return qContainerElement.closest(QContainerSelector); +export function _getQContainerElement(element: Element): Element | null { + return element.closest(QContainerSelector); } export const isDomContainer = (container: any): container is DomContainer => { @@ -99,7 +92,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer { public qManifestHash: string; public rootVNode: ElementVNode; public document: QDocument; - public $journal$: VNodeJournal; public $rawStateData$: unknown[]; public $storeProxyMap$: ObjToProxyMap = new WeakMap(); public $qFuncs$: Array<(...args: unknown[]) => unknown>; @@ -111,29 +103,14 @@ export class DomContainer extends _SharedContainer implements IClientContainer { private $styleIds$: Set | null = null; constructor(element: ContainerElement) { - super( - () => { - this.$flushEpoch$++; - vnode_applyJournal(this.$journal$); - }, - {}, - element.getAttribute(QLocaleAttr)! - ); + super({}, element.getAttribute(QLocaleAttr)!); this.qContainer = element.getAttribute(QContainerAttr)!; if (!this.qContainer) { throw qError(QError.elementWithoutContainer); } - this.$journal$ = [ - // The first time we render we need to hoist the styles. - // (Meaning we need to move all styles from component inline to ) - // We bulk move all of the styles, because the expensive part is - // for the browser to recompute the styles, (not the actual DOM manipulation.) - // By moving all of them at once we can minimize the reflow. - VNodeJournalOpCode.HoistStyles, - element.ownerDocument, - ]; this.document = element.ownerDocument as QDocument; this.element = element; + this.$hoistStyles$(); this.$buildBase$ = element.getAttribute(QBaseAttr)!; this.$instanceHash$ = element.getAttribute(QInstanceAttr)!; this.qManifestHash = element.getAttribute(QManifestHashAttr)!; @@ -160,7 +137,24 @@ export class DomContainer extends _SharedContainer implements IClientContainer { } } - $setRawState$(id: number, vParent: ElementVNode | VirtualVNode): void { + /** + * The first time we render we need to hoist the styles. (Meaning we need to move all styles from + * component inline to ) + * + * We bulk move all of the styles, because the expensive part is for the browser to recompute the + * styles, (not the actual DOM manipulation.) By moving all of them at once we can minimize the + * reflow. + */ + $hoistStyles$(): void { + const document = this.element.ownerDocument; + const head = document.head; + const styles = document.querySelectorAll(QStylesAllSelector); + for (let i = 0; i < styles.length; i++) { + head.appendChild(styles[i]); + } + } + + $setRawState$(id: number, vParent: VNode): void { this.$stateData$[id] = vParent; } @@ -171,17 +165,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer { handleError(err: any, host: VNode | null): void { if (qDev && host) { if (typeof document !== 'undefined') { - const vHost = host as VirtualVNode; - const journal: VNodeJournal = []; + const vHost = host; const vHostParent = vHost.parent; const vHostNextSibling = vHost.nextSibling as VNode | null; - const vErrorDiv = vnode_createErrorDiv(document, vHost, err, journal); + const journal: VNodeJournal = []; + const vErrorDiv = vnode_createErrorDiv(journal, document, vHost, err); // If the host is an element node, we need to insert the error div into its parent. const insertHost = vnode_isElementVNode(vHost) ? vHostParent || vHost : vHost; // If the host is different then we need to insert errored-host in the same position as the host. const insertBefore = insertHost === vHost ? null : vHostNextSibling; - vnode_insertBefore(journal, insertHost, vErrorDiv, insertBefore); - vnode_applyJournal(journal); + vnode_insertBefore( + journal, + insertHost as ElementVNode | VirtualVNode, + vErrorDiv, + insertBefore + ); } if (err && err instanceof Error) { @@ -223,7 +221,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { let vNode: VNode | null = host.parent; while (vNode) { if (vnode_isVirtualVNode(vNode)) { - if (vNode.getProp(OnRenderProp, null) !== null) { + if (vnode_getProp(vNode, OnRenderProp, null) !== null) { return vNode; } vNode = @@ -239,7 +237,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { setHostProp(host: HostElement, name: string, value: T): void { const vNode: VirtualVNode = host as any; - vNode.setProp(name, value); + vnode_setProp(vNode, name, value); } getHostProp(host: HostElement, name: string): T | null { @@ -258,20 +256,21 @@ export class DomContainer extends _SharedContainer implements IClientContainer { getObjectById = parseInt; break; } - return vNode.getProp(name, getObjectById); + return vnode_getProp(vNode, name, getObjectById); } ensureProjectionResolved(vNode: VirtualVNode): void { if ((vNode.flags & VNodeFlags.Resolved) === 0) { vNode.flags |= VNodeFlags.Resolved; - const props = vnode_getProps(vNode); - for (let i = 0; i < props.length; i = i + 2) { - const prop = props[i] as string; - if (isSlotProp(prop)) { - const value = props[i + 1]; - if (typeof value == 'string') { - const projection = this.vNodeLocate(value); - props[i + 1] = projection; + const props = vNode.props; + if (props) { + for (const prop of Object.keys(props)) { + if (isSlotProp(prop)) { + const value = props[prop]; + if (typeof value == 'string') { + const projection = this.vNodeLocate(value); + props[prop] = projection; + } } } } @@ -307,7 +306,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { const styleElement = this.document.createElement('style'); styleElement.setAttribute(QStyle, styleId); styleElement.textContent = content; - this.$journal$.push(VNodeJournalOpCode.Insert, this.document.head, null, styleElement); + this.document.head.appendChild(styleElement); } } diff --git a/packages/qwik/src/core/client/dom-render.ts b/packages/qwik/src/core/client/dom-render.ts index b93a3a38e21..3dad03ccacb 100644 --- a/packages/qwik/src/core/client/dom-render.ts +++ b/packages/qwik/src/core/client/dom-render.ts @@ -1,13 +1,15 @@ -import type { FunctionComponent, JSXNode, JSXOutput } from '../shared/jsx/types/jsx-node'; +import type { FunctionComponent, JSXOutput } from '../shared/jsx/types/jsx-node'; import { isDocument, isElement } from '../shared/utils/element'; -import { ChoreType } from '../shared/util-chore-type'; import { QContainerValue } from '../shared/types'; import { DomContainer, getDomContainer } from './dom-container'; import { cleanup } from './vnode-diff'; -import { QContainerAttr } from '../shared/utils/markers'; +import { NODE_DIFF_DATA_KEY, QContainerAttr } from '../shared/utils/markers'; import type { RenderOptions, RenderResult } from './types'; import { qDev } from '../shared/utils/qdev'; import { QError, qError } from '../shared/error/error'; +import { vnode_setProp } from './vnode'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; /** * Render JSX. @@ -42,11 +44,16 @@ export const render = async ( const container = getDomContainer(parent as HTMLElement) as DomContainer; container.$serverData$ = opts.serverData || {}; const host = container.rootVNode; - container.$scheduler$(ChoreType.NODE_DIFF, host, host, jsxNode as JSXNode); - await container.$scheduler$(ChoreType.WAIT_FOR_QUEUE).$returnValue$; + vnode_setProp(host, NODE_DIFF_DATA_KEY, jsxNode); + markVNodeDirty(container, host, ChoreBits.NODE_DIFF); + await container.$renderPromise$; return { cleanup: () => { - cleanup(container, container.rootVNode); + /** + * This can lead to cleaning up projection vnodes via the journal, but since we're cleaning up + * they don't matter so we ignore the journal + */ + cleanup(container, [], container.rootVNode); }, }; }; diff --git a/packages/qwik/src/core/client/process-vnode-data.ts b/packages/qwik/src/core/client/process-vnode-data.ts index 2278abaebfa..b5993572fa7 100644 --- a/packages/qwik/src/core/client/process-vnode-data.ts +++ b/packages/qwik/src/core/client/process-vnode-data.ts @@ -1,7 +1,7 @@ // NOTE: we want to move this function to qwikloader, and therefore this function should not have any external dependencies import { VNodeDataChar, VNodeDataSeparator } from '../shared/vnode-data-types'; import type { ContainerElement, QDocument } from './types'; -import type { ElementVNode } from './vnode-impl'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; /** * Process the VNodeData script tags and store the VNodeData in the VNodeDataMap. diff --git a/packages/qwik/src/core/client/run-qrl.ts b/packages/qwik/src/core/client/run-qrl.ts index e0c717edcd2..90d370f8a3a 100644 --- a/packages/qwik/src/core/client/run-qrl.ts +++ b/packages/qwik/src/core/client/run-qrl.ts @@ -1,11 +1,9 @@ -import { QError, qError } from '../shared/error/error'; import type { QRLInternal } from '../shared/qrl/qrl-class'; -import { getChorePromise } from '../shared/scheduler'; -import { ChoreType } from '../shared/util-chore-type'; +import { retryOnPromise } from '../shared/utils/promises'; import type { ValueOrPromise } from '../shared/utils/types'; import { getInvokeContext } from '../use/use-core'; import { useLexicalScope } from '../use/use-lexical-scope.public'; -import { getDomContainer } from './dom-container'; +import { VNodeFlags } from './types'; /** * This is called by qwik-loader to run a QRL. It has to be synchronous. @@ -17,20 +15,11 @@ export const _run = (...args: unknown[]): ValueOrPromise => { const [runQrl] = useLexicalScope<[QRLInternal<(...args: unknown[]) => unknown>]>(); const context = getInvokeContext(); const hostElement = context.$hostElement$; - - if (!hostElement) { - // silently ignore if there is no host element, the element might have been removed - return; + if (hostElement) { + return retryOnPromise(() => { + if (!(hostElement.flags & VNodeFlags.Deleted)) { + return runQrl(...args); + } + }); } - - const container = getDomContainer(context.$element$!); - - const scheduler = container.$scheduler$; - if (!scheduler) { - throw qError(QError.schedulerNotFound); - } - - // We don't return anything, the scheduler is in charge now - const chore = scheduler(ChoreType.RUN_QRL, hostElement, runQrl, args); - return getChorePromise(chore); }; diff --git a/packages/qwik/src/core/client/types.ts b/packages/qwik/src/core/client/types.ts index 4bf341e23c5..6d04971ab42 100644 --- a/packages/qwik/src/core/client/types.ts +++ b/packages/qwik/src/core/client/types.ts @@ -2,8 +2,8 @@ import type { QRL } from '../shared/qrl/qrl.public'; import type { Container } from '../shared/types'; -import type { VNodeJournal } from './vnode'; -import type { ElementVNode, VirtualVNode } from './vnode-impl'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; +import type { VirtualVNode } from '../shared/vnode/virtual-vnode'; export type ClientAttrKey = string; export type ClientAttrValue = string | null; @@ -17,9 +17,7 @@ export interface ClientContainer extends Container { $locale$: string; qManifestHash: string; rootVNode: ElementVNode; - $journal$: VNodeJournal; $forwardRefs$: Array | null; - $flushEpoch$: number; parseQRL(qrl: string): QRL; $setRawState$(id: number, vParent: ElementVNode | VirtualVNode): void; } @@ -74,30 +72,32 @@ export interface QDocument extends Document { * @internal */ export const enum VNodeFlags { - Element /* ****************** */ = 0b00_000001, - Virtual /* ****************** */ = 0b00_000010, - ELEMENT_OR_VIRTUAL_MASK /* ** */ = 0b00_000011, - Text /* ********************* */ = 0b00_000100, - ELEMENT_OR_TEXT_MASK /* ***** */ = 0b00_000101, - TYPE_MASK /* **************** */ = 0b00_000111, - INFLATED_TYPE_MASK /* ******* */ = 0b00_001111, + Element /* ****************** */ = 0b00_0000001, + Virtual /* ****************** */ = 0b00_0000010, + ELEMENT_OR_VIRTUAL_MASK /* ** */ = 0b00_0000011, + Text /* ********************* */ = 0b00_0000100, + ELEMENT_OR_TEXT_MASK /* ***** */ = 0b00_0000101, + TYPE_MASK /* **************** */ = 0b00_0000111, + INFLATED_TYPE_MASK /* ******* */ = 0b00_0001111, /// Extra flag which marks if a node needs to be inflated. - Inflated /* ***************** */ = 0b00_001000, + Inflated /* ***************** */ = 0b00_0001000, /// Marks if the `ensureProjectionResolved` has been called on the node. - Resolved /* ***************** */ = 0b00_010000, + Resolved /* ***************** */ = 0b00_0010000, /// Marks if the vnode is deleted. - Deleted /* ****************** */ = 0b00_100000, + Deleted /* ****************** */ = 0b00_0100000, + /// Marks if the vnode is a cursor (has priority set). + Cursor /* ******************* */ = 0b00_1000000, /// Flags for Namespace - NAMESPACE_MASK /* *********** */ = 0b11_000000, - NEGATED_NAMESPACE_MASK /* ** */ = ~0b11_000000, - NS_html /* ****************** */ = 0b00_000000, // http://www.w3.org/1999/xhtml - NS_svg /* ******************* */ = 0b01_000000, // http://www.w3.org/2000/svg - NS_math /* ****************** */ = 0b10_000000, // http://www.w3.org/1998/Math/MathML + NAMESPACE_MASK /* *********** */ = 0b11_0000000, + NEGATED_NAMESPACE_MASK /* ** */ = ~0b11_0000000, + NS_html /* ****************** */ = 0b00_0000000, // http://www.w3.org/1999/xhtml + NS_svg /* ******************* */ = 0b01_0000000, // http://www.w3.org/2000/svg + NS_math /* ****************** */ = 0b10_0000000, // http://www.w3.org/1998/Math/MathML } export const enum VNodeFlagsIndex { - mask /* ************** */ = 0b11_111111, - shift /* ************* */ = 8, + mask /* ************** */ = 0b11_1111111, + shift /* ************* */ = 9, } export const enum VNodeProps { diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index 029b3d0fac4..4d513b17b94 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -1,5 +1,4 @@ import { isDev } from '@qwik.dev/core/build'; -import { _CONST_PROPS, _EFFECT_BACK_REF, _VAR_PROPS } from '../internal'; import { clearAllEffects, clearEffectSubscription } from '../reactive-primitives/cleanup'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; import type { Signal } from '../reactive-primitives/signal.public'; @@ -27,13 +26,11 @@ import type { JSXNodeInternal } from '../shared/jsx/types/jsx-node'; import type { JSXChildren } from '../shared/jsx/types/jsx-qwik-attributes'; import { SSRComment, SSRRaw, SkipRender } from '../shared/jsx/utils.public'; import type { QRLInternal } from '../shared/qrl/qrl-class'; -import { isSyncQrl } from '../shared/qrl/qrl-utils'; import type { QRL } from '../shared/qrl/qrl.public'; import type { HostElement, QElement, QwikLoaderEventScope, qWindow } from '../shared/types'; import { DEBUG_TYPE, QContainerValue, VirtualType } from '../shared/types'; -import { ChoreType } from '../shared/util-chore-type'; import { escapeHTML } from '../shared/utils/character-escaping'; -import { _OWNER, _PROPS_HANDLER } from '../shared/utils/constants'; +import { _CONST_PROPS, _OWNER, _PROPS_HANDLER, _VAR_PROPS } from '../shared/utils/constants'; import { fromCamelToKebabCase, getEventDataFromHtmlAttribute, @@ -43,7 +40,6 @@ import { } from '../shared/utils/event-names'; import { getFileLocationFromJsx } from '../shared/utils/jsx-filename'; import { - ELEMENT_KEY, ELEMENT_PROPS, ELEMENT_SEQ, OnRenderProp, @@ -54,24 +50,22 @@ import { QTemplate, dangerouslySetInnerHTML, } from '../shared/utils/markers'; -import { isPromise, retryOnPromise } from '../shared/utils/promises'; +import { isPromise, retryOnPromise, safeCall } from '../shared/utils/promises'; import { isSlotProp } from '../shared/utils/prop'; import { hasClassAttr } from '../shared/utils/scoped-styles'; import { serializeAttribute } from '../shared/utils/styles'; import { isArray, isObject, type ValueOrPromise } from '../shared/utils/types'; import { trackSignalAndAssignHost } from '../use/use-core'; import { TaskFlags, isTask } from '../use/use-task'; -import type { DomContainer } from './dom-container'; -import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types'; -import { mapApp_findIndx, mapArray_set } from './util-mapArray'; +import { VNodeFlags, type ClientContainer } from './types'; +import { mapApp_findIndx } from './util-mapArray'; import { - VNodeJournalOpCode, vnode_ensureElementInflated, vnode_getDomParentVNode, vnode_getElementName, vnode_getFirstChild, vnode_getProjectionParentComponent, - vnode_getProps, + vnode_getProp, vnode_getText, vnode_getType, vnode_insertBefore, @@ -85,26 +79,33 @@ import { vnode_newText, vnode_newVirtual, vnode_remove, + vnode_setAttr, + vnode_setProp, vnode_setText, vnode_truncate, vnode_walkVNode, type VNodeJournal, } from './vnode'; -import { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl'; import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace'; import { cleanupDestroyable } from '../use/utils/destroyable'; import { SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { isStore } from '../reactive-primitives/impl/store'; import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl'; +import type { VNode } from '../shared/vnode/vnode'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; +import type { VirtualVNode } from '../shared/vnode/virtual-vnode'; +import type { TextVNode } from '../shared/vnode/text-vnode'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; +import { _EFFECT_BACK_REF } from '../reactive-primitives/backref'; export const vnode_diff = ( container: ClientContainer, + journal: VNodeJournal, jsxNode: JSXChildren, vStartNode: VNode, scopedStyleIdPrefix: string | null ) => { - let journal = (container as DomContainer).$journal$; - /** * Stack is used to keep track of the state of the traversal. * @@ -247,7 +248,6 @@ export const vnode_diff = ( } } else if (jsxValue === (SkipRender as JSXChildren)) { // do nothing, we are skipping this node - journal = []; } else { expectText(''); } @@ -415,14 +415,15 @@ export const vnode_diff = ( const projections: Array = []; if (host) { - const props = vnode_getProps(host); - // we need to create empty projections for all the slots to remove unused slots content - for (let i = 0; i < props.length; i = i + 2) { - const prop = props[i] as string; - if (isSlotProp(prop)) { - const slotName = prop; - projections.push(slotName); - projections.push(createProjectionJSXNode(slotName)); + const props = host.props; + if (props) { + // we need to create empty projections for all the slots to remove unused slots content + for (const prop of Object.keys(props)) { + if (isSlotProp(prop)) { + const slotName = prop; + projections.push(slotName); + projections.push(createProjectionJSXNode(slotName)); + } } } } @@ -462,7 +463,7 @@ export const vnode_diff = ( const slotName = jsxNode.key as string; // console.log('expectProjection', JSON.stringify(slotName)); // The parent is the component and it should have our portal. - vCurrent = (vParent as VirtualVNode).getProp(slotName, (id) => + vCurrent = vnode_getProp(vParent as VirtualVNode, slotName, (id: string) => vnode_locate(container.rootVNode, id) ); // if projection is marked as deleted then we need to create a new one @@ -473,11 +474,11 @@ export const vnode_diff = ( // that is wrong. We don't yet know if the projection will be projected, so // we should leave it unattached. // vNewNode[VNodeProps.parent] = vParent; - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection); - isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectProjection'); - (vNewNode as VirtualVNode).setProp(QSlot, slotName); + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection); + isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectProjection'); + vnode_setProp(vNewNode as VirtualVNode, QSlot, slotName); (vNewNode as VirtualVNode).slotParent = vParent; - (vParent as VirtualVNode).setProp(slotName, vNewNode); + vnode_setProp(vParent as VirtualVNode, slotName, vNewNode); } } @@ -485,17 +486,17 @@ export const vnode_diff = ( const vHost = vnode_getProjectionParentComponent(vParent); const slotNameKey = getSlotNameKey(vHost); - // console.log('expectSlot', JSON.stringify(slotNameKey)); const vProjectedNode = vHost - ? (vHost as VirtualVNode).getProp( + ? vnode_getProp( + vHost as VirtualVNode, slotNameKey, // for slots this id is vnode ref id null // Projections should have been resolved through container.ensureProjectionResolved //(id) => vnode_locate(container.rootVNode, id) ) : null; - // console.log(' ', String(vHost), String(vProjectedNode)); + if (vProjectedNode == null) { // Nothing to project, so render content of the slot. vnode_insertBefore( @@ -504,14 +505,13 @@ export const vnode_diff = ( (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() ); - (vNewNode as VirtualVNode).setProp(QSlot, slotNameKey); - vHost && (vHost as VirtualVNode).setProp(slotNameKey, vNewNode); - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection); - isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectSlot' + count++); + vnode_setProp(vNewNode as VirtualVNode, QSlot, slotNameKey); + vHost && vnode_setProp(vHost as VirtualVNode, slotNameKey, vNewNode); + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection); + isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectSlot' + count++); return false; } else if (vProjectedNode === vCurrent) { // All is good. - // console.log(' NOOP', String(vCurrent)); } else { // move from q:template to the target node vnode_insertBefore( @@ -520,10 +520,10 @@ export const vnode_diff = ( (vNewNode = vProjectedNode), vCurrent && getInsertBefore() ); - (vNewNode as VirtualVNode).setProp(QSlot, slotNameKey); - vHost && (vHost as VirtualVNode).setProp(slotNameKey, vNewNode); - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Projection); - isDev && (vNewNode as VirtualVNode).setProp('q:code', 'expectSlot' + count++); + vnode_setProp(vNewNode as VirtualVNode, QSlot, slotNameKey); + vHost && vnode_setProp(vHost as VirtualVNode, slotNameKey, vNewNode); + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Projection); + isDev && vnode_setProp(vNewNode as VirtualVNode, 'q:code', 'expectSlot' + count++); } return true; } @@ -547,7 +547,7 @@ export const vnode_diff = ( if (vNode.flags & VNodeFlags.Deleted) { continue; } - cleanup(container, vNode); + cleanup(container, journal, vNode); vnode_remove(journal, vParent, vNode, true); } vSideBuffer.clear(); @@ -590,10 +590,10 @@ export const vnode_diff = ( if (vFirstChild !== null) { let vChild: VNode | null = vFirstChild; while (vChild) { - cleanup(container, vChild); + cleanup(container, journal, vChild); vChild = vChild.nextSibling as VNode | null; } - vnode_truncate(journal, vCurrent as ElementVNode | VirtualVNode, vFirstChild); + vnode_truncate(container, journal, vCurrent as ElementVNode | VirtualVNode, vFirstChild); } } @@ -605,7 +605,7 @@ export const vnode_diff = ( const toRemove = vCurrent; advanceToNextSibling(); if (vParent === toRemove.parent) { - cleanup(container, toRemove); + cleanup(container, journal, toRemove); // If we are diffing projection than the parent is not the parent of the node. // If that is the case we don't want to remove the node from the parent. vnode_remove(journal, vParent, toRemove, true); @@ -616,7 +616,7 @@ export const vnode_diff = ( function expectNoMoreTextNodes() { while (vCurrent !== null && vnode_isTextVNode(vCurrent)) { - cleanup(container, vCurrent); + cleanup(container, journal, vCurrent); const toRemove = vCurrent; advanceToNextSibling(); vnode_remove(journal, vParent, toRemove, true); @@ -667,10 +667,10 @@ export const vnode_diff = ( const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent); if (eventName) { - vNewNode!.setProp(HANDLER_PREFIX + ':' + scopedEvent, value); + vnode_setProp(vNewNode!, HANDLER_PREFIX + ':' + scopedEvent, value); if (scope) { // window and document need attrs so qwik loader can find them - vNewNode!.setAttr(key, '', journal); + vnode_setAttr(journal, vNewNode!, key, ''); } // register an event for qwik loader (window/document prefixed with '-') registerQwikLoaderEvent(loaderScopedEvent); @@ -736,7 +736,7 @@ export const vnode_diff = ( } const key = jsx.key; if (key) { - (vNewNode as ElementVNode).setProp(ELEMENT_KEY, key); + (vNewNode as ElementVNode).key = key; } // append class attribute if styleScopedId exists and there is no class attribute @@ -771,7 +771,7 @@ export const vnode_diff = ( vCurrent && vnode_isElementVNode(vCurrent) && elementName === vnode_getElementName(vCurrent); const jsxKey: string | null = jsx.key; let needsQDispatchEventPatch = false; - const currentKey = getKey(vCurrent); + const currentKey = getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null); if (!isSameElementName || jsxKey !== currentKey) { const sideBufferKey = getSideBufferKey(elementName, jsxKey); const createNew = () => (needsQDispatchEventPatch = createNewElement(jsx, elementName)); @@ -783,37 +783,23 @@ export const vnode_diff = ( // reconcile attributes - const jsxAttrs = [] as ClientAttrs; - const props = jsx.varProps; - if (jsx.toSort) { - const keys = Object.keys(props).sort(); - for (const key of keys) { - const value = props[key]; - if (value != null) { - jsxAttrs.push(key, value as any); - } - } - } else { - for (const key in props) { - const value = props[key]; - if (value != null) { - jsxAttrs.push(key, value as any); - } - } - } - if (jsxKey !== null) { - mapArray_set(jsxAttrs, ELEMENT_KEY, jsxKey, 0); - } + const jsxProps = jsx.varProps; const vNode = (vNewNode || vCurrent) as ElementVNode; - const element = vNode.element as QElement; + const element = vNode.node as QElement; if (!element.vNode) { element.vNode = vNode; } - needsQDispatchEventPatch = - setBulkProps(vNode, jsxAttrs, (isDev && getFileLocationFromJsx(jsx.dev)) || null) || - needsQDispatchEventPatch; + if (jsxProps) { + needsQDispatchEventPatch = + diffProps( + vNode, + jsxProps, + (vNode.props ||= {}), + (isDev && getFileLocationFromJsx(jsx.dev)) || null + ) || needsQDispatchEventPatch; + } if (needsQDispatchEventPatch) { // Event handler needs to be patched onto the element. if (!element.qDispatchEvent) { @@ -821,97 +807,56 @@ export const vnode_diff = ( const eventName = fromCamelToKebabCase(event.type); const eventProp = ':' + scope.substring(1) + ':' + eventName; const qrls = [ - vNode.getProp(eventProp, null), - vNode.getProp(HANDLER_PREFIX + eventProp, null), + vnode_getProp(vNode, eventProp, null), + vnode_getProp(vNode, HANDLER_PREFIX + eventProp, null), ]; - let returnValue = false; - qrls.flat(2).forEach((qrl) => { + + for (const qrl of qrls.flat(2)) { if (qrl) { - if (isSyncQrl(qrl)) { - qrl(event, element); - } else { - const value = container.$scheduler$( - ChoreType.RUN_QRL, - vNode, - qrl as QRLInternal<(...args: unknown[]) => unknown>, - [event, element] - ) as unknown; - returnValue = returnValue || value === true; - } + safeCall( + () => qrl(event, element), + () => {}, + (e) => { + container.handleError(e, vNode); + } + ); } - }); - return returnValue; + } }; } } } - /** @returns True if `qDispatchEvent` needs patching */ - function setBulkProps( + function diffProps( vnode: ElementVNode, - srcAttrs: ClientAttrs, + newAttrs: Record, + oldAttrs: Record, currentFile: string | null ): boolean { vnode_ensureElementInflated(vnode); - const dstAttrs = vnode_getProps(vnode) as ClientAttrs; - let srcIdx = 0; - let dstIdx = 0; let patchEventDispatch = false; - /** - * Optimized setAttribute that bypasses redundant checks when we already know: - * - * - The index in dstAttrs (no need for binary search) - * - The vnode is ElementVNode (no instanceof check) - * - The value has changed (no comparison needed) - */ - const setAttributeDirect = ( - vnode: ElementVNode, - key: string, - value: any, - dstIdx: number, - isNewKey: boolean - ) => { + const setAttribute = (vnode: ElementVNode, key: string, value: any) => { const serializedValue = value != null ? serializeAttribute(key, value, scopedStyleIdPrefix) : null; - - if (isNewKey) { - // Adding new key - splice into sorted position - if (serializedValue != null) { - (dstAttrs as any).splice(dstIdx, 0, key, serializedValue); - journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue); - } - } else { - // Updating or removing existing key at dstIdx - if (serializedValue != null) { - // Update existing value - (dstAttrs as any)[dstIdx + 1] = serializedValue; - journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, serializedValue); - } else { - // Remove key (value is null) - dstAttrs.splice(dstIdx, 2); - journal.push(VNodeJournalOpCode.SetAttribute, vnode.element, key, null); - } - } + vnode_setAttr(journal, vnode, key, serializedValue); }; - const record = (key: string, value: any, dstIdx: number, isNewKey: boolean) => { + const record = (key: string, value: any) => { if (key.startsWith(':')) { - vnode.setProp(key, value); + vnode_setProp(vnode, key, value); return; } if (key === 'ref') { - const element = vnode.element; + const element = vnode.node; if (isSignal(value)) { value.value = element; return; } else if (typeof value === 'function') { value(element); return; - } - // handling null value is not needed here, because we are filtering null values earlier - else { + } else { throw qError(QError.invalidRefValue, [currentFile]); } } @@ -925,8 +870,6 @@ export const vnode_diff = ( return; } if (currentEffect) { - // Clear current effect subscription if it exists - // Only if we want to track the signal again clearEffectSubscription(container, currentEffect); } @@ -942,29 +885,20 @@ export const vnode_diff = ( ); } else { if (currentEffect) { - // Clear current effect subscription if it exists - // and the value is not a signal - // It means that the previous value was a signal and we need to clear the effect subscription clearEffectSubscription(container, currentEffect); } } if (isPromise(value)) { - // For async values, we can't use the known index since it will be stale by the time - // the promise resolves. Do a binary search to find the current index. const vHost = vnode as ElementVNode; const attributePromise = value.then((resolvedValue) => { - const idx = mapApp_findIndx(dstAttrs, key, 0); - const isNewKey = idx < 0; - const currentDstIdx = isNewKey ? idx ^ -1 : idx; - setAttributeDirect(vHost, key, resolvedValue, currentDstIdx, isNewKey); + setAttribute(vHost, key, resolvedValue); }); asyncAttributePromises.push(attributePromise); return; } - // Always use optimized direct path - we know the index from the merge algorithm - setAttributeDirect(vnode, key, value, dstIdx, isNewKey); + setAttribute(vnode, key, value); }; const recordJsxEvent = (key: string, value: any) => { @@ -973,86 +907,38 @@ export const vnode_diff = ( const [scope, eventName] = data; const scopedEvent = getScopedEventName(scope, eventName); const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent); - // Pass dummy index values since ':' prefixed keys take early return via setProp - record(':' + scopedEvent, value, 0, false); - // register an event for qwik loader (window/document prefixed with '-') + record(':' + scopedEvent, value); registerQwikLoaderEvent(loaderScopedEvent); patchEventDispatch = true; } }; - // Two-pointer merge algorithm: both arrays are sorted by key - // Note: dstAttrs mutates during iteration (setAttr uses splice), so we re-read keys each iteration - while (srcIdx < srcAttrs.length || dstIdx < dstAttrs.length) { - const srcKey = srcIdx < srcAttrs.length ? (srcAttrs[srcIdx] as string) : undefined; - const dstKey = dstIdx < dstAttrs.length ? (dstAttrs[dstIdx] as string) : undefined; + // Actual diffing logic + // Apply all new attributes + for (const key in newAttrs) { + const newValue = newAttrs[key]; + const isEvent = isHtmlAttributeAnEventName(key); - // Skip special keys in destination HANDLER_PREFIX - if (dstKey?.startsWith(HANDLER_PREFIX)) { - dstIdx += 2; // skip key and value - continue; + if (key in oldAttrs) { + if (newValue !== oldAttrs[key]) { + isEvent ? recordJsxEvent(key, newValue) : record(key, newValue); + } + } else if (newValue != null) { + isEvent ? recordJsxEvent(key, newValue) : record(key, newValue); } + } - if (srcKey === undefined) { - // Source exhausted: remove remaining destination keys - if (isHtmlAttributeAnEventName(dstKey!)) { - // HTML event attributes are immutable and not removed from DOM - dstIdx += 2; // skip key and value - } else { - record(dstKey!, null, dstIdx, false); - // After removal, dstAttrs shrinks by 2, so don't advance dstIdx - } - } else if (dstKey === undefined) { - // Destination exhausted: add remaining source keys - const srcValue = srcAttrs[srcIdx + 1]; - if (isHtmlAttributeAnEventName(srcKey)) { - recordJsxEvent(srcKey, srcValue); - } else { - record(srcKey, srcValue, dstIdx, true); - } - srcIdx += 2; // skip key and value - // After addition, dstAttrs grows by 2 at sorted position, advance dstIdx - dstIdx += 2; - } else if (srcKey === dstKey) { - // Keys match: update if values differ - const srcValue = srcAttrs[srcIdx + 1]; - const dstValue = dstAttrs[dstIdx + 1]; - const isEventHandler = isHtmlAttributeAnEventName(srcKey); - if (srcValue !== dstValue) { - if (isEventHandler) { - recordJsxEvent(srcKey, srcValue); - } else { - record(srcKey, srcValue, dstIdx, false); - } - } else if (isEventHandler && !vnode.element.qDispatchEvent) { - // Special case: add event handlers after resume - recordJsxEvent(srcKey, srcValue); - } - // Update in place doesn't change array length - srcIdx += 2; // skip key and value - dstIdx += 2; // skip key and value - } else if (srcKey < dstKey) { - // Source has a key not in destination: add it - const srcValue = srcAttrs[srcIdx + 1]; - if (isHtmlAttributeAnEventName(srcKey)) { - recordJsxEvent(srcKey, srcValue); - } else { - record(srcKey, srcValue, dstIdx, true); - } - srcIdx += 2; // skip key and value - // After addition, dstAttrs grows at sorted position (before dstIdx), advance dstIdx - dstIdx += 2; - } else { - // Destination has a key not in source: remove it (dstKey > srcKey) - if (isHtmlAttributeAnEventName(dstKey)) { - // HTML event attributes are immutable and not removed from DOM - dstIdx += 2; // skip key and value - } else { - record(dstKey, null, dstIdx, false); - // After removal, dstAttrs shrinks at dstIdx, so don't advance dstIdx - } + // Remove attributes that no longer exist in new props + for (const key in oldAttrs) { + if ( + !(key in newAttrs) && + !key.startsWith(HANDLER_PREFIX) && + !isHtmlAttributeAnEventName(key) + ) { + record(key, null); } } + return patchEventDispatch; } @@ -1075,7 +961,9 @@ export const vnode_diff = ( let vNode = vCurrent; while (vNode) { const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null; - const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$); + const vKey = + getKey(vNode as VirtualVNode | ElementVNode | TextVNode | null) || + getComponentHash(vNode, container.$getObjectById$); if (vNodeWithKey === null && vKey == key && name == nodeName) { vNodeWithKey = vNode as ElementVNode | VirtualVNode; } else { @@ -1115,7 +1003,9 @@ export const vnode_diff = ( if (!targetNode) { if (vCurrent) { const name = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) : null; - const vKey = getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$); + const vKey = + getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null) || + getComponentHash(vCurrent, container.$getObjectById$); if (vKey != null) { const sideBufferKey = getSideBufferKey(name, vKey); vSideBuffer ||= new Map(); @@ -1131,7 +1021,9 @@ export const vnode_diff = ( let vNode = vCurrent; while (vNode && vNode !== targetNode) { const name = vnode_isElementVNode(vNode) ? vnode_getElementName(vNode) : null; - const vKey = getKey(vNode) || getComponentHash(vNode, container.$getObjectById$); + const vKey = + getKey(vNode as VirtualVNode | ElementVNode | TextVNode | null) || + getComponentHash(vNode, container.$getObjectById$); if (vKey != null) { const sideBufferKey = getSideBufferKey(name, vKey); @@ -1197,7 +1089,8 @@ export const vnode_diff = ( vSideBuffer!.delete(sideBufferKey); if (addCurrentToSideBufferOnSideInsert && vCurrent) { const currentKey = - getKey(vCurrent) || getComponentHash(vCurrent, container.$getObjectById$); + getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null) || + getComponentHash(vCurrent, container.$getObjectById$); if (currentKey != null) { const currentName = vnode_isElementVNode(vCurrent) ? vnode_getElementName(vCurrent) @@ -1209,7 +1102,12 @@ export const vnode_diff = ( } } } - vnode_insertBefore(journal, parentForInsert as any, buffered, vCurrent); + vnode_insertBefore( + journal, + parentForInsert as ElementVNode | VirtualVNode, + buffered, + vCurrent + ); vCurrent = buffered; vNewNode = null; return; @@ -1222,7 +1120,7 @@ export const vnode_diff = ( function expectVirtual(type: VirtualType, jsxKey: string | null) { const checkKey = type === VirtualType.Fragment; - const currentKey = getKey(vCurrent); + const currentKey = getKey(vCurrent as VirtualVNode | ElementVNode | TextVNode | null); const currentIsVirtual = vCurrent && vnode_isVirtualVNode(vCurrent); const isSameNode = currentIsVirtual && currentKey === jsxKey && (checkKey ? !!jsxKey : true); @@ -1239,8 +1137,8 @@ export const vnode_diff = ( (vNewNode = vnode_newVirtual()), vCurrent && getInsertBefore() ); - (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxKey); - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, type); + (vNewNode as VirtualVNode).key = jsxKey; + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, type); }; // For fragments without a key, always create a new virtual node (ensures rerender semantics) if (jsxKey === null) { @@ -1293,7 +1191,8 @@ export const vnode_diff = ( } if (host) { - const vNodeProps = (host as VirtualVNode).getProp( + const vNodeProps = vnode_getProp( + host as VirtualVNode, ELEMENT_PROPS, container.$getObjectById$ ); @@ -1304,7 +1203,7 @@ export const vnode_diff = ( if (shouldRender) { // Assign the new QRL instance to the host. // Unfortunately it is created every time, something to fix in the optimizer. - (host as VirtualVNode).setProp(OnRenderProp, componentQRL); + vnode_setProp(host as VirtualVNode, OnRenderProp, componentQRL); /** * Mark host as not deleted. The host could have been marked as deleted if it there was a @@ -1312,7 +1211,7 @@ export const vnode_diff = ( * deleted. */ (host as VirtualVNode).flags &= ~VNodeFlags.Deleted; - container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, vNodeProps); + markVNodeDirty(container, host as VirtualVNode, ChoreBits.COMPONENT); } } descendContentToProject(jsxNode.children, host); @@ -1344,7 +1243,8 @@ export const vnode_diff = ( while ( componentHost && (vnode_isVirtualVNode(componentHost) - ? (componentHost as VirtualVNode).getProp | null>( + ? vnode_getProp | null>( + componentHost as VirtualVNode, OnRenderProp, null ) === null @@ -1381,10 +1281,10 @@ export const vnode_diff = ( vCurrent && getInsertBefore() ); const jsxNode = jsxValue as JSXNodeInternal; - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.Component); - container.setHostProp(vNewNode, OnRenderProp, componentQRL); - container.setHostProp(vNewNode, ELEMENT_PROPS, jsxProps); - container.setHostProp(vNewNode, ELEMENT_KEY, jsxNode.key); + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.Component); + vnode_setProp(vNewNode as VirtualVNode, OnRenderProp, componentQRL); + vnode_setProp(vNewNode as VirtualVNode, ELEMENT_PROPS, jsxProps); + (vNewNode as VirtualVNode).key = jsxNode.key; } function insertNewInlineComponent() { @@ -1395,10 +1295,10 @@ export const vnode_diff = ( vCurrent && getInsertBefore() ); const jsxNode = jsxValue as JSXNodeInternal; - isDev && (vNewNode as VirtualVNode).setProp(DEBUG_TYPE, VirtualType.InlineComponent); - (vNewNode as VirtualVNode).setProp(ELEMENT_PROPS, jsxNode.props); + isDev && vnode_setProp(vNewNode as VirtualVNode, DEBUG_TYPE, VirtualType.InlineComponent); + vnode_setProp(vNewNode as VirtualVNode, ELEMENT_PROPS, jsxNode.props); if (jsxNode.key) { - (vNewNode as VirtualVNode).setProp(ELEMENT_KEY, jsxNode.key); + (vNewNode as VirtualVNode).key = jsxNode.key; } } @@ -1428,11 +1328,11 @@ export const vnode_diff = ( * @param vNode - VNode to retrieve the key from * @returns Key */ -function getKey(vNode: VNode | null): string | null { - if (vNode == null) { +function getKey(vNode: VirtualVNode | ElementVNode | TextVNode | null): string | null { + if (vNode == null || vnode_isTextVNode(vNode)) { return null; } - return (vNode as VirtualVNode).getProp(ELEMENT_KEY, null); + return vNode.key; } /** @@ -1443,10 +1343,10 @@ function getKey(vNode: VNode | null): string | null { * @returns Hash */ function getComponentHash(vNode: VNode | null, getObject: (id: string) => any): string | null { - if (vNode == null) { + if (vNode == null || vnode_isTextVNode(vNode)) { return null; } - const qrl = (vNode as VirtualVNode).getProp(OnRenderProp, getObject); + const qrl = vnode_getProp(vNode as VirtualVNode, OnRenderProp, getObject); return qrl ? qrl.$hash$ : null; } @@ -1484,7 +1384,6 @@ function handleProps( container: ClientContainer ): boolean { let shouldRender = false; - let propsAreDifferent = false; if (vNodeProps) { const effects = vNodeProps[_PROPS_HANDLER].$effects$; const constPropsDifferent = handleChangedProps( @@ -1494,33 +1393,24 @@ function handleProps( container, false ); - propsAreDifferent = constPropsDifferent; shouldRender ||= constPropsDifferent; if (effects && effects.size > 0) { - const varPropsDifferent = handleChangedProps( + handleChangedProps( jsxProps[_VAR_PROPS], vNodeProps[_VAR_PROPS], vNodeProps[_PROPS_HANDLER], - container + container, + true ); - - propsAreDifferent ||= varPropsDifferent; // don't mark as should render, effects will take care of it - // shouldRender ||= varPropsDifferent; - } - } - - if (propsAreDifferent) { - if (vNodeProps) { - // Reuse the same props instance, qrls can use the current props instance - // as a capture ref, so we can't change it. - vNodeProps[_OWNER] = (jsxProps as PropsProxy)[_OWNER]; - } else if (jsxProps) { - // If there is no props instance, create a new one. - // We can do this because we are not using the props instance for anything else. - (host as VirtualVNode).setProp(ELEMENT_PROPS, jsxProps); - vNodeProps = jsxProps; } + // Update the owner after all props have been synced + vNodeProps[_OWNER] = (jsxProps as PropsProxy)[_OWNER]; + } else if (jsxProps) { + // If there is no props instance, create a new one. + // We can do this because we are not using the props instance for anything else. + vnode_setProp(host as VirtualVNode, ELEMENT_PROPS, jsxProps); + vNodeProps = jsxProps; } return shouldRender; } @@ -1532,51 +1422,49 @@ function handleChangedProps( container: ClientContainer, triggerEffects: boolean = true ): boolean { - const srcEmpty = isPropsEmpty(src); - const dstEmpty = isPropsEmpty(dst); - - if (srcEmpty && dstEmpty) { + if (isPropsEmpty(src) && isPropsEmpty(dst)) { return false; } - if (srcEmpty || dstEmpty) { - return true; - } - - const srcKeys = Object.keys(src!); - const dstKeys = Object.keys(dst!); - - let srcLen = srcKeys.length; - let dstLen = dstKeys.length; - if ('children' in src!) { - srcLen--; - } - if (QBackRefs in src!) { - srcLen--; - } - if ('children' in dst!) { - dstLen--; - } - if (QBackRefs in dst!) { - dstLen--; - } + propsHandler.$container$ = container; + let changed = false; - if (srcLen !== dstLen) { - return true; + // Update changed/added props from src + if (src) { + for (const key in src) { + if (key === 'children' || key === QBackRefs) { + continue; + } + if (!dst || src[key] !== dst[key]) { + changed = true; + if (triggerEffects) { + if (dst) { + // Update the value in dst BEFORE triggering effects + // so effects see the new value + // Note: Value is not triggering effects, because we are modyfing direct VAR_PROPS object + dst[key] = src[key]; + } + triggerPropsProxyEffect(propsHandler, key); + } else { + // Early return for const props (no effects) + return true; + } + } + } } - let changed = false; - propsHandler.$container$ = container; - for (const key of srcKeys) { - if (key === 'children' || key === QBackRefs) { - continue; - } - if (!Object.prototype.hasOwnProperty.call(dst, key) || src![key] !== dst![key]) { - changed = true; - if (triggerEffects) { - triggerPropsProxyEffect(propsHandler, key); - } else { - return true; + // Remove props that are in dst but not in src + if (dst) { + for (const key in dst) { + if (key === 'children' || key === QBackRefs) { + continue; + } + if (!src || !(key in src)) { + changed = true; + if (triggerEffects) { + delete dst[key]; + triggerPropsProxyEffect(propsHandler, key); + } } } } @@ -1601,7 +1489,7 @@ function isPropsEmpty(props: Record | null | undefined): boolean { * - Projection nodes by not recursing into them. * - Component nodes by recursing into the component content nodes (which may be projected). */ -export function cleanup(container: ClientContainer, vNode: VNode) { +export function cleanup(container: ClientContainer, journal: VNodeJournal, vNode: VNode) { let vCursor: VNode | null = vNode; // Depth first traversal if (vnode_isTextVNode(vNode)) { @@ -1618,7 +1506,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { const isComponent = type & VNodeFlags.Virtual && - (vCursor as VirtualVNode).getProp | null>(OnRenderProp, null) !== null; + vnode_getProp | null>(vCursor as VirtualVNode, OnRenderProp, null) !== null; if (isComponent) { // cleanup q:seq content const seq = container.getHostProp>(vCursor as VirtualVNode, ELEMENT_SEQ); @@ -1628,7 +1516,9 @@ export function cleanup(container: ClientContainer, vNode: VNode) { if (isObject(obj)) { const objIsTask = isTask(obj); if (objIsTask && obj.$flags$ & TaskFlags.VISIBLE_TASK) { - container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj); + obj.$flags$ |= TaskFlags.DIRTY; + markVNodeDirty(container, vCursor, ChoreBits.CLEANUP); + // don't call cleanupDestroyable yet, do it by the scheduler continue; } else if (obj instanceof SignalImpl || isStore(obj)) { @@ -1643,24 +1533,25 @@ export function cleanup(container: ClientContainer, vNode: VNode) { } // SPECIAL CASE: If we are a component, we need to descend into the projected content and release the content. - const attrs = vnode_getProps(vCursor as VirtualVNode); - for (let i = 0; i < attrs.length; i = i + 2) { - const key = attrs[i] as string; - if (isSlotProp(key)) { - const value = attrs[i + 1]; - if (value) { - attrs[i + 1] = null; // prevent infinite loop - const projection = - typeof value === 'string' - ? vnode_locate(container.rootVNode, value) - : (value as unknown as VNode); - let projectionChild = vnode_getFirstChild(projection); - while (projectionChild) { - cleanup(container, projectionChild); - projectionChild = projectionChild.nextSibling as VNode | null; - } + const attrs = (vCursor as VirtualVNode).props; + if (attrs) { + for (const key of Object.keys(attrs)) { + if (isSlotProp(key)) { + const value = attrs[key]; + if (value) { + attrs[key] = null; // prevent infinite loop + const projection = + typeof value === 'string' + ? vnode_locate(container.rootVNode, value) + : (value as unknown as VNode); + let projectionChild = vnode_getFirstChild(projection); + while (projectionChild) { + cleanup(container, journal, projectionChild); + projectionChild = projectionChild.nextSibling as VNode | null; + } - cleanupStaleUnclaimedProjection(container.$journal$, projection); + cleanupStaleUnclaimedProjection(journal, projection); + } } } } @@ -1675,7 +1566,9 @@ export function cleanup(container: ClientContainer, vNode: VNode) { vCursor = vFirstChild; continue; } - } else if (vCursor === vNode) { + } + // TODO: probably can be removed + else if (vCursor === vNode) { /** * If it is a projection and we are at the root, then we should only walk the children to * materialize the projection content. This is because we could have references in the vnode diff --git a/packages/qwik/src/core/client/vnode-diff.unit.tsx b/packages/qwik/src/core/client/vnode-diff.unit.tsx index f4636c4e051..fa2098c1f5f 100644 --- a/packages/qwik/src/core/client/vnode-diff.unit.tsx +++ b/packages/qwik/src/core/client/vnode-diff.unit.tsx @@ -1,4 +1,5 @@ import { + $, Fragment, _fnSignal, _jsxSorted, @@ -20,72 +21,71 @@ import type { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-sign import { createSignal } from '../reactive-primitives/signal-api'; import { StoreFlags } from '../reactive-primitives/types'; import { QError, qError } from '../shared/error/error'; -import type { Scheduler } from '../shared/scheduler'; import type { QElement } from '../shared/types'; -import { ChoreType } from '../shared/util-chore-type'; import { VNodeFlags } from './types'; -import { vnode_applyJournal, vnode_getFirstChild, vnode_getNode } from './vnode'; +import { vnode_getFirstChild, vnode_getNode, type VNodeJournal } from './vnode'; import { vnode_diff } from './vnode-diff'; -import type { VirtualVNode } from './vnode-impl'; - -async function waitForDrain(scheduler: Scheduler) { - await scheduler(ChoreType.WAIT_FOR_QUEUE).$returnValue$; -} +import { _flushJournal } from '../shared/cursor/cursor-flush'; describe('vNode-diff', () => { it('should find no difference', () => { const { vNode, vParent, container } = vnode_fromJSX(
Hello
); expect(vNode).toMatchVDOM(
Hello
); - expect(vnode_getNode(vNode!)!.ownerDocument!.body.innerHTML).toEqual( - '
Hello
' - ); - vnode_diff(container,
Hello
, vParent, null); - expect(container.$journal$.length).toEqual(0); + expect(vnode_getNode(vNode!)!.ownerDocument!.body.innerHTML).toEqual('
Hello
'); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
Hello
, vParent, null); + expect(journal.length).toEqual(0); }); describe('text', () => { it('should update text', () => { const { vNode, vParent, container } = vnode_fromJSX(
Hello
); - vnode_diff(container,
World
, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
World
, vParent, null); expect(vNode).toMatchVDOM(
World
); - expect(container.$journal$).not.toEqual([]); - expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); - vnode_applyJournal(container.$journal$); - expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
World
'); + expect(journal).not.toEqual([]); + expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); + _flushJournal(journal); + expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
World
'); }); it('should add missing text node', () => { const { vNode, vParent, container } = vnode_fromJSX(
); - vnode_diff(container,
Hello
, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
Hello
, vParent, null); expect(vNode).toMatchVDOM(
Hello
); - expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
'); - vnode_applyJournal(container.$journal$); - expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); + expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
'); + _flushJournal(journal); + expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); }); it('should update and add missing text node', () => { const { vNode, vParent, container } = vnode_fromJSX(
text
); - vnode_diff(container,
Hello {'World'}
, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
Hello {'World'}
, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(
Hello {'World'}
); }); it('should remove extra text nodes', () => { const { vNode, vParent, container } = vnode_fromJSX(
text{'removeMe'}
); - vnode_diff(container,
Hello
, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
Hello
, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(
Hello
); }); it('should remove all text nodes', () => { const { vNode, vParent, container } = vnode_fromJSX(
text{'removeMe'}
); - vnode_diff(container,
, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(
); }); it('should treat undefined as no children', () => { const { vNode, vParent, container } = vnode_fromJSX(
text{'removeMe'}
); - vnode_diff(container,
{undefined}
, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
{undefined}
, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(
); }); }); @@ -103,9 +103,10 @@ describe('vNode-diff', () => { ); - vnode_diff(container, test, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); expect(vNode).toMatchVDOM(test); - expect(container.$journal$.length).toEqual(0); + expect(journal.length).toEqual(0); }); it('should add missing element', () => { const { vNode, vParent, container } = vnode_fromJSX(); @@ -114,8 +115,9 @@ describe('vNode-diff', () => { ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); it('should remove extra text node', async () => { @@ -131,8 +133,9 @@ describe('vNode-diff', () => { ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); await expect(container.document.querySelector('test')).toMatchDOM(test); }); @@ -166,8 +169,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -196,8 +200,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -226,8 +231,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -256,8 +262,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -277,8 +284,9 @@ describe('vNode-diff', () => { ) ); const test = _jsxSorted('span', {}, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -307,8 +315,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -337,8 +346,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -367,8 +377,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); @@ -388,8 +399,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); }); @@ -397,20 +409,28 @@ describe('vNode-diff', () => { describe('keys', () => { it('should not reuse element because old has a key and new one does not', () => { const { vNode, vParent, container } = vnode_fromJSX( - _jsxSorted('test', {}, null, [_jsxSorted('b', {}, null, 'old', 0, '1')], 0, 'KA_6') + _jsxSorted( + 'test', + {}, + null, + [_jsxSorted('b', { id: 'b1' }, null, 'old', 0, '1')], + 0, + 'KA_6' + ) ); const test = _jsxSorted( 'test', {}, null, - [_jsxSorted('b', {}, null, 'new', 0, null)], + [_jsxSorted('b', { id: 'b1' }, null, 'new', 0, null)], 0, 'KA_6' ); - const bOriginal = container.document.querySelector('b[q\\:key=1]')!; + const bOriginal = container.document.querySelector('#b1')!; expect(bOriginal).toBeDefined(); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); const bSecond = container.document.querySelector('b')!; expect(bSecond).toBeDefined(); @@ -422,7 +442,10 @@ describe('vNode-diff', () => { 'test', {}, null, - [_jsxSorted('b', {}, null, '1', 0, '1'), _jsxSorted('b', {}, null, '2', 0, '2')], + [ + _jsxSorted('b', { id: 'b1' }, null, '1', 0, '1'), + _jsxSorted('b', { id: 'b2' }, null, '2', 0, '2'), + ], 0, 'KA_6' ) @@ -433,23 +456,24 @@ describe('vNode-diff', () => { null, [ _jsxSorted('b', {}, null, 'before', 0, null), - _jsxSorted('b', {}, null, '2', 0, '2'), + _jsxSorted('b', { id: 'b2' }, null, '2', 0, '2'), _jsxSorted('b', {}, null, '3', 0, '3'), _jsxSorted('b', {}, null, 'in', 0, null), - _jsxSorted('b', {}, null, '1', 0, '1'), + _jsxSorted('b', { id: 'b1' }, null, '1', 0, '1'), _jsxSorted('b', {}, null, 'after', 0, null), ], 0, 'KA_6' ); - const selectB1 = () => container.document.querySelector('b[q\\:key=1]')!; - const selectB2 = () => container.document.querySelector('b[q\\:key=2]')!; + const selectB1 = () => container.document.querySelector('b#b1')!; + const selectB2 = () => container.document.querySelector('b#b2')!; const b1 = selectB1(); const b2 = selectB2(); expect(b1).toBeDefined(); expect(b2).toBeDefined(); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); expect(b1).toBe(selectB1()); expect(b2).toBe(selectB2()); @@ -474,8 +498,9 @@ describe('vNode-diff', () => { 0, 'KA_6' ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); }); }); @@ -494,11 +519,12 @@ describe('vNode-diff', () => { null ); - const signalFragment = vnode_getFirstChild(vNode!) as VirtualVNode; + const signalFragment = vnode_getFirstChild(vNode!); expect(signalFragment).toBeDefined(); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(expected); expect(signalFragment).toBe(vnode_getFirstChild(vNode!)); }); @@ -517,11 +543,12 @@ describe('vNode-diff', () => { null ); - const promiseFragment = vnode_getFirstChild(vNode!) as VirtualVNode; + const promiseFragment = vnode_getFirstChild(vNode!); expect(promiseFragment).toBeDefined(); - await vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + await vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(expected); expect(promiseFragment).toBe(vnode_getFirstChild(vNode!)); }); @@ -539,11 +566,12 @@ describe('vNode-diff', () => { null ); - const fragment = vnode_getFirstChild(vNode!) as VirtualVNode; + const fragment = vnode_getFirstChild(vNode!); expect(fragment).toBeDefined(); - await vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vNode).toMatchVDOM(test); expect(fragment).not.toBe(vnode_getFirstChild(vNode!)); }); @@ -552,8 +580,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX('1'); const test = Promise.resolve('2') as unknown as JSXOutput; //_jsxSorted(Fragment, {}, null, ['1'], 0, null); - await vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + await vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vParent).toMatchVDOM( 2 @@ -566,8 +595,9 @@ describe('vNode-diff', () => { it('should set attributes', async () => { const { vParent, container } = vnode_fromJSX(); const test = _jsxSorted('span', {}, { class: 'abcd', id: 'b' }, null, 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); const firstChildNode = vnode_getNode(firstChild) as Element; await expect(firstChildNode).toMatchDOM(test); @@ -590,8 +620,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -610,8 +641,9 @@ describe('vNode-diff', () => { ) ); const test = _jsxSorted('span', {}, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -640,8 +672,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -660,8 +693,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(); }); @@ -680,18 +714,20 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(); }); - it('should add qDispatchEvent for existing html event attribute', () => { + it('should not add qDispatchEvent for removed html event attribute', () => { const { vParent, container } = vnode_fromJSX( _jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'a' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -702,9 +738,10 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX( _jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null) ); - const test = _jsxSorted('span', { id: 'a', onClick$: 'abcd' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const test = _jsxSorted('span', { id: 'a', onClick$: $(() => {}) }, null, [], 0, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -716,8 +753,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { id: 'a' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'a', onClick$: () => null }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -729,8 +767,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { id: 'a', 'on:click': 'abcd' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'a', onClick$: () => null }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -742,8 +781,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { id: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { name: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -753,8 +793,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { id: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { name: 'bbb', title: 'ccc' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -764,8 +805,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { id: 'aaa', title: 'ccc' }, null, [], 0, null) ); const test = _jsxSorted('span', { name: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -775,8 +817,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { name: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -786,8 +829,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { name: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'bbb', title: 'ccc' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -797,8 +841,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { name: 'aaa', title: 'ccc' }, null, [], 0, null) ); const test = _jsxSorted('span', { id: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); }); @@ -808,8 +853,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { onDblClick$: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { onClick$: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -821,8 +867,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { onClick$: 'aaa' }, null, [], 0, null) ); const test = _jsxSorted('span', { onDblClick$: 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -832,8 +879,9 @@ describe('vNode-diff', () => { it('should add event scope to element add qDispatchEvent', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const test = _jsxSorted('span', { 'window:onClick$': 'bbb' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); const element = vnode_getNode(firstChild) as QElement; @@ -845,8 +893,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal = createSignal(); const test = _jsxSorted('span', { ref: signal }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); expect(signal.value).toBe(vnode_getNode(firstChild)); @@ -863,8 +912,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); expect((globalThis as any).node).toBe(vnode_getNode(firstChild)); @@ -874,8 +924,9 @@ describe('vNode-diff', () => { it('should handle null ref value attribute', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const test = _jsxSorted('span', { ref: null }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); }); @@ -884,7 +935,8 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const test = _jsxSorted('span', { ref: 'abc' }, null, [], 0, null); expect(() => { - vnode_diff(container, test, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); }).toThrowError(qError(QError.invalidRefValue, [null])); }); }); @@ -894,8 +946,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal = createSignal('test'); const test = _jsxSorted('span', { class: signal }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); }); @@ -905,8 +958,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal = createSignal('initial') as SignalImpl; const test1 = _jsxSorted('span', { class: signal }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -916,9 +970,9 @@ describe('vNode-diff', () => { // Replace signal with regular string value const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify effects have been cleaned up @@ -929,8 +983,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal1 = createSignal('first') as SignalImpl; const test1 = _jsxSorted('span', { class: signal1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -941,9 +996,9 @@ describe('vNode-diff', () => { // Replace with another signal const signal2 = createSignal('second') as SignalImpl; const test2 = _jsxSorted('span', { class: signal2 }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify first signal's effects have been cleaned up @@ -957,8 +1012,9 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal = createSignal('test') as SignalImpl; const test1 = _jsxSorted('span', { class: signal }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -968,9 +1024,9 @@ describe('vNode-diff', () => { // Remove the attribute entirely const test2 = _jsxSorted('span', {}, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify effects have been cleaned up @@ -988,8 +1044,9 @@ describe('vNode-diff', () => { '() => inner.value' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1002,9 +1059,9 @@ describe('vNode-diff', () => { // Replace wrapped signal with regular string value const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify inner signal's effects have been cleaned up @@ -1022,8 +1079,9 @@ describe('vNode-diff', () => { '() => inner1.value' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1042,9 +1100,9 @@ describe('vNode-diff', () => { '() => inner2.value' ) as WrappedSignalImpl; const test2 = _jsxSorted('span', { class: wrapped2 }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify first inner signal's effects have been cleaned up @@ -1068,8 +1126,9 @@ describe('vNode-diff', () => { '() => inner.value' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1082,9 +1141,9 @@ describe('vNode-diff', () => { // Remove the attribute entirely const test2 = _jsxSorted('span', {}, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify effects have been cleaned up @@ -1103,8 +1162,9 @@ describe('vNode-diff', () => { '() => store.cls' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1116,9 +1176,9 @@ describe('vNode-diff', () => { // Replace wrapped signal with regular string value const test2 = _jsxSorted('span', { class: 'static' }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify store's effects have been cleaned up @@ -1136,8 +1196,9 @@ describe('vNode-diff', () => { '() => store1.cls' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1154,9 +1215,9 @@ describe('vNode-diff', () => { '() => store2.cls' ) as WrappedSignalImpl; const test2 = _jsxSorted('span', { class: wrapped2 }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify first store/ wrapped effects have been cleaned up @@ -1177,8 +1238,9 @@ describe('vNode-diff', () => { '() => store.cls' ) as WrappedSignalImpl; const test1 = _jsxSorted('span', { class: wrapped }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); @@ -1189,9 +1251,9 @@ describe('vNode-diff', () => { // Remove the attribute entirely const test2 = _jsxSorted('span', {}, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); // Verify effects have been cleaned up @@ -1224,9 +1286,10 @@ describe('vNode-diff', () => { 3, null ) as any; - vnode_diff(container, test1, vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + await vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); + await container.$renderPromise$; // Ensure one subscription exists for both wrapped and inner expect(wrapped.$effects$).not.toBeNull(); @@ -1245,10 +1308,10 @@ describe('vNode-diff', () => { 3, null ) as any; - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + await vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); + await container.$renderPromise$; // The number of effects should not increase (no duplicate subscriptions) expect(wrapped.$effects$!.size).toBe(wrappedEffectsAfterFirst); @@ -1268,8 +1331,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1285,8 +1349,9 @@ describe('vNode-diff', () => { ) ); const test = _jsxSorted('span', {}, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1302,8 +1367,9 @@ describe('vNode-diff', () => { ) ); const test = _jsxSorted('span', { class: 'test', id: 'b' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1319,8 +1385,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1336,8 +1403,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1346,8 +1414,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3', z: '26' }, null, [], 0, null) ); const test = _jsxSorted('span', { z: '26' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1356,8 +1425,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { z: '26' }, null, [], 0, null) ); const test = _jsxSorted('span', { a: '1', b: '2', c: '3', z: '26' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1366,8 +1436,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { b: '2', d: '4', f: '6' }, null, [], 0, null) ); const test = _jsxSorted('span', { a: '1', c: '3', e: '5' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1390,8 +1461,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); const element = vnode_getNode(firstChild) as QElement; @@ -1410,8 +1482,9 @@ describe('vNode-diff', () => { ) ); const test = _jsxSorted('span', { id: 'test' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(test); const element = vnode_getNode(firstChild) as QElement; @@ -1430,8 +1503,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); const element = vnode_getNode(firstChild) as QElement; expect(element.qDispatchEvent).toBeDefined(); @@ -1442,8 +1516,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null) ); const test = _jsxSorted('span', { a: '10', b: '20', c: '30' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1452,9 +1527,10 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null) ); const test = _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null); - vnode_diff(container, test, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); // Journal should be empty since no changes were made - expect(container.$journal$.length).toEqual(0); + expect(journal.length).toEqual(0); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); @@ -1464,8 +1540,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null) ); const test1 = _jsxSorted('span', { a: 'NEW', b: '2', c: '3' }, null, [], 0, null); - vnode_diff(container1, test1, vParent1, null); - vnode_applyJournal(container1.$journal$); + const journal1: VNodeJournal = []; + vnode_diff(container1, journal1, test1, vParent1, null); + _flushJournal(journal1); expect(vnode_getFirstChild(vParent1)).toMatchVDOM(test1); // Change middle @@ -1473,8 +1550,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null) ); const test2 = _jsxSorted('span', { a: '1', b: 'NEW', c: '3' }, null, [], 0, null); - vnode_diff(container2, test2, vParent2, null); - vnode_applyJournal(container2.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container2, journal2, test2, vParent2, null); + _flushJournal(journal2); expect(vnode_getFirstChild(vParent2)).toMatchVDOM(test2); // Change last @@ -1482,8 +1560,9 @@ describe('vNode-diff', () => { _jsxSorted('span', { a: '1', b: '2', c: '3' }, null, [], 0, null) ); const test3 = _jsxSorted('span', { a: '1', b: '2', c: 'NEW' }, null, [], 0, null); - vnode_diff(container3, test3, vParent3, null); - vnode_applyJournal(container3.$journal$); + const journal3: VNodeJournal = []; + vnode_diff(container3, journal3, test3, vParent3, null); + _flushJournal(journal3); expect(vnode_getFirstChild(vParent3)).toMatchVDOM(test3); }); @@ -1497,8 +1576,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); const element = vnode_getNode(firstChild) as QElement; @@ -1534,8 +1614,9 @@ describe('vNode-diff', () => { 0, null ); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); const element = vnode_getNode(firstChild) as QElement; @@ -1546,17 +1627,18 @@ describe('vNode-diff', () => { const { vParent, container } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); const signal1 = createSignal('value1'); const test1 = _jsxSorted('span', { class: signal1 }, null, [], 0, null); - vnode_diff(container, test1, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); const firstChild = vnode_getFirstChild(vParent); expect(firstChild).toMatchVDOM(); // Update with different signal const signal2 = createSignal('value2'); const test2 = _jsxSorted('span', { class: signal2 }, null, [], 0, null); - container.$journal$ = []; - vnode_diff(container, test2, vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, test2, vParent, null); + _flushJournal(journal2); expect(firstChild).toMatchVDOM(); }); @@ -1573,8 +1655,9 @@ describe('vNode-diff', () => { _jsxSorted('span', manyAttrs, null, [], 0, null) ); const test = _jsxSorted('span', manyAttrsUpdated, null, [], 0, null); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(vnode_getFirstChild(vParent)).toMatchVDOM(test); }); }); @@ -1587,11 +1670,12 @@ describe('vNode-diff', () => { vParent.flags |= VNodeFlags.Deleted; - vnode_diff(container,
World
, vParent, null); + const journal: VNodeJournal = []; + vnode_diff(container, journal,
World
, vParent, null); - expect(container.$journal$.length).toEqual(0); + expect(journal.length).toEqual(0); - expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); + expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('
Hello
'); }); }); @@ -1601,14 +1685,16 @@ describe('vNode-diff', () => { const signal = createSignal('test') as SignalImpl; const test = _jsxSorted('div', { class: signal }, null, [], 0, 'KA_0'); - vnode_diff(container, test, vParent, null); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + _flushJournal(journal); expect(signal.$effects$).toBeDefined(); expect(signal.$effects$!.size).toBeGreaterThan(0); - vnode_diff(container, _jsxSorted('div', {}, null, [], 0, 'KA_0'), vParent, null); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + vnode_diff(container, journal2, _jsxSorted('div', {}, null, [], 0, 'KA_0'), vParent, null); + _flushJournal(journal2); expect(signal.$effects$!.size).toBe(0); }); @@ -1623,16 +1709,18 @@ describe('vNode-diff', () => { }); const test1 = _jsxSorted(Child, null, { value: signal }, null, 3, null) as JSXChildren; - await vnode_diff(container, test1, vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + await vnode_diff(container, journal, test1, vParent, null); + _flushJournal(journal); + await container.$renderPromise$; expect(signal.$effects$).toBeDefined(); expect(signal.$effects$!.size).toBeGreaterThan(0); - await vnode_diff(container, , vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + await vnode_diff(container, journal2, , vParent, null); + _flushJournal(journal2); + await container.$renderPromise$; expect(signal.$effects$!.size).toBe(0); }); @@ -1648,16 +1736,18 @@ describe('vNode-diff', () => { }); const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any; - vnode_diff(container, test, vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + await container.$renderPromise$; + _flushJournal(journal); expect((globalThis as any).innerSignal.$effects$).toBeDefined(); expect((globalThis as any).innerSignal.$effects$!.size).toBeGreaterThan(0); - vnode_diff(container, , vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal2: VNodeJournal = []; + await vnode_diff(container, journal2, , vParent, null); + await container.$renderPromise$; + _flushJournal(journal2); expect((globalThis as any).innerSignal.$effects$.size).toBe(0); }); @@ -1673,19 +1763,19 @@ describe('vNode-diff', () => { }); const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any; - vnode_diff(container, test, vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); + const journal: VNodeJournal = []; + vnode_diff(container, journal, test, vParent, null); + await container.$renderPromise$; + _flushJournal(journal); const store = getStoreHandler((globalThis as any).store); expect(store!.$effects$?.size).toBeGreaterThan(0); - container.$journal$ = []; - vnode_diff(container, , vParent, null); - await waitForDrain(container.$scheduler$); - vnode_applyJournal(container.$journal$); - + const journal2: VNodeJournal = []; + await vnode_diff(container, journal2, , vParent, null); + _flushJournal(journal2); + await container.$renderPromise$; expect(store!.$effects$?.size).toBe(0); }); }); diff --git a/packages/qwik/src/core/client/vnode-impl.ts b/packages/qwik/src/core/client/vnode-impl.ts index 9d948a39820..79b0e20f28b 100644 --- a/packages/qwik/src/core/client/vnode-impl.ts +++ b/packages/qwik/src/core/client/vnode-impl.ts @@ -7,10 +7,9 @@ import { type VNodeJournal, } from './vnode'; import type { ChoreArray } from './chore-array'; -import { _EFFECT_BACK_REF } from '../reactive-primitives/types'; -import { BackRef } from '../reactive-primitives/cleanup'; import { isDev } from '@qwik.dev/core/build'; import type { QElement } from '../shared/types'; +import { BackRef } from '../reactive-primitives/backref'; /** @internal */ export abstract class VNode extends BackRef { diff --git a/packages/qwik/src/core/client/vnode-namespace.ts b/packages/qwik/src/core/client/vnode-namespace.ts index 8f7970a789a..76d52c3e732 100644 --- a/packages/qwik/src/core/client/vnode-namespace.ts +++ b/packages/qwik/src/core/client/vnode-namespace.ts @@ -21,7 +21,10 @@ import { vnode_isTextVNode, type VNodeJournal, } from './vnode'; -import type { ElementVNode, VNode } from './vnode-impl'; +import type { ElementVNode } from '../shared/vnode/element-vnode'; +import type { VNode } from '../shared/vnode/vnode'; +import type { TextVNode } from '../shared/vnode/text-vnode'; +import type { Container } from '../shared/types'; export const isForeignObjectElement = (elementName: string) => { return isDev ? elementName.toLowerCase() === 'foreignobject' : elementName === 'foreignObject'; @@ -53,16 +56,16 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( journal: VNodeJournal, domParentVNode: ElementVNode, newChild: VNode -) { +): (ElementVNode | TextVNode)[] { const { elementNamespace, elementNamespaceFlag } = getNewElementNamespaceData( domParentVNode, newChild ); - let domChildren: (Element | Text)[] = []; + let domChildren: (ElementVNode | TextVNode)[] = []; if (elementNamespace === HTML_NS) { // parent is in the default namespace, so just get the dom children. This is the fast path. - domChildren = vnode_getDOMChildNodes(journal, newChild); + domChildren = vnode_getDOMChildNodes(journal, newChild, true); } else { // parent is in a different namespace, so we need to clone the children with the correct namespace. // The namespace cannot be changed on nodes, so we need to clone these nodes @@ -72,7 +75,7 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( const childVNode = children[i]; if (vnode_isTextVNode(childVNode)) { // text nodes are always in the default namespace - domChildren.push(childVNode.textNode as Text); + domChildren.push(childVNode); continue; } if ( @@ -80,7 +83,7 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( (domParentVNode.flags & VNodeFlags.NAMESPACE_MASK) ) { // if the child and parent have the same namespace, we don't need to clone the element - domChildren.push(childVNode.element as Element); + domChildren.push(childVNode); continue; } @@ -93,7 +96,8 @@ export function vnode_getDomChildrenWithCorrectNamespacesToInsert( ); if (newChildElement) { - domChildren.push(newChildElement); + childVNode.node = newChildElement; + domChildren.push(childVNode); } } } @@ -154,7 +158,7 @@ function vnode_cloneElementWithNamespace( let newChildElement: Element | null = null; if (vnode_isElementVNode(vCursor)) { // Clone the element - childElement = vCursor.element as Element; + childElement = vCursor.node; const childElementTag = vnode_getElementName(vCursor); // We need to check if the parent is a foreignObject element @@ -197,7 +201,7 @@ function vnode_cloneElementWithNamespace( // Then we can overwrite the cursor with newly created element. // This is because we need to materialize the children before we assign new element - vCursor.element = newChildElement; + vCursor.node = newChildElement; // Set correct namespace flag vCursor.flags &= VNodeFlags.NEGATED_NAMESPACE_MASK; vCursor.flags |= namespaceFlag; diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index a1a574fb3a9..99840f1e160 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -126,6 +126,7 @@ import { QContainerValue, VirtualType, VirtualTypeName, + type Container, type QElement, } from '../shared/types'; import { isText } from '../shared/utils/element'; @@ -153,7 +154,6 @@ import { import { isHtmlElement } from '../shared/utils/types'; import { VNodeDataChar } from '../shared/vnode-data-types'; import { getDomContainer } from './dom-container'; -import { mapArray_set } from './util-mapArray'; import { type ClientContainer, type ContainerElement, @@ -166,50 +166,42 @@ import { vnode_getElementNamespaceFlags, } from './vnode-namespace'; import { mergeMaps } from '../shared/utils/maps'; -import { _EFFECT_BACK_REF } from '../reactive-primitives/types'; -import { ElementVNode, TextVNode, VirtualVNode, VNode } from './vnode-impl'; import { EventNameHtmlScope } from '../shared/utils/event-names'; +import { VNode } from '../shared/vnode/vnode'; +import { ElementVNode } from '../shared/vnode/element-vnode'; +import { TextVNode } from '../shared/vnode/text-vnode'; +import { VirtualVNode } from '../shared/vnode/virtual-vnode'; +import { VNodeOperationType } from '../shared/vnode/enums/vnode-operation-type.enum'; +import { addVNodeOperation } from '../shared/vnode/vnode-dirty'; +import { isCursor } from '../shared/cursor/cursor'; +import { _EFFECT_BACK_REF } from '../reactive-primitives/backref'; +import type { VNodeOperation } from '../shared/vnode/types/dom-vnode-operation'; +import { _flushJournal } from '../shared/cursor/cursor-flush'; ////////////////////////////////////////////////////////////////////////////////////////////////////// -/** - * Fundamental DOM operations are: - * - * - Insert new DOM element/text - * - Remove DOM element/text - * - Set DOM element attributes - * - Set text node value - */ -export const enum VNodeJournalOpCode { - SetText = 1, // ------ [SetAttribute, target, text] - SetAttribute = 2, // - [SetAttribute, target, ...(key, values)]] - HoistStyles = 3, // -- [HoistStyles, document] - Remove = 4, // ------- [Remove, target(parent), ...nodes] - RemoveAll = 5, // ------- [RemoveAll, target(parent)] - Insert = 6, // ------- [Insert, target(parent), reference, ...nodes] -} - -export type VNodeJournal = Array< - VNodeJournalOpCode | Document | Element | Text | string | boolean | null ->; +export type VNodeJournal = Array; ////////////////////////////////////////////////////////////////////////////////////////////////////// -export const vnode_newElement = (element: Element, elementName: string): ElementVNode => { +export const vnode_newElement = ( + element: Element, + elementName: string, + key: string | null = null +): ElementVNode => { assertEqual(fastNodeType(element), 1 /* ELEMENT_NODE */, 'Expecting element node.'); const vnode: ElementVNode = new ElementVNode( + key, VNodeFlags.Element | VNodeFlags.Inflated | (-1 << VNodeFlagsIndex.shift), // Flag null, null, null, null, null, + null, element, elementName ); - assertTrue(vnode_isElementVNode(vnode), 'Incorrect format of ElementVNode.'); - assertFalse(vnode_isTextVNode(vnode), 'Incorrect format of ElementVNode.'); - assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of ElementVNode.'); (element as QElement).vNode = vnode; return vnode; }; @@ -217,18 +209,17 @@ export const vnode_newElement = (element: Element, elementName: string): Element export const vnode_newUnMaterializedElement = (element: Element): ElementVNode => { assertEqual(fastNodeType(element), 1 /* ELEMENT_NODE */, 'Expecting element node.'); const vnode: ElementVNode = new ElementVNode( + null, VNodeFlags.Element | (-1 << VNodeFlagsIndex.shift), // Flag null, null, null, + null, undefined, undefined, element, undefined ); - assertTrue(vnode_isElementVNode(vnode), 'Incorrect format of ElementVNode.'); - assertFalse(vnode_isTextVNode(vnode), 'Incorrect format of ElementVNode.'); - assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of ElementVNode.'); (element as QElement).vNode = vnode; return vnode; }; @@ -245,12 +236,10 @@ export const vnode_newSharedText = ( null, // Parent previousTextNode, // Previous TextNode (usually first child) null, // Next sibling - sharedTextNode, // SharedTextNode - textContent // Text Content + null, + sharedTextNode, + textContent ); - assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.'); - assertTrue(vnode_isTextVNode(vnode), 'Incorrect format of TextVNode.'); - assertFalse(vnode_isVirtualVNode(vnode), 'Incorrect format of TextVNode.'); return vnode; }; @@ -260,6 +249,7 @@ export const vnode_newText = (textNode: Text, textContent: string | undefined): null, // Parent null, // No previous sibling null, // We may have a next sibling. + null, textNode, // TextNode textContent // Text Content ); @@ -272,11 +262,13 @@ export const vnode_newText = (textNode: Text, textContent: string | undefined): export const vnode_newVirtual = (): VirtualVNode => { const vnode: VirtualVNode = new VirtualVNode( + null, VNodeFlags.Virtual | (-1 << VNodeFlagsIndex.shift), // Flags null, null, null, null, + null, null ); assertFalse(vnode_isElementVNode(vnode), 'Incorrect format of TextVNode.'); @@ -292,12 +284,10 @@ export const vnode_isVNode = (vNode: any): vNode is VNode => { }; export const vnode_isElementVNode = (vNode: VNode): vNode is ElementVNode => { - assertDefined(vNode, 'Missing vNode'); - const flag = vNode.flags; - return (flag & VNodeFlags.Element) === VNodeFlags.Element; + return vNode instanceof ElementVNode; }; -export const vnode_isElementOrTextVNode = (vNode: VNode): vNode is ElementVNode => { +export const vnode_isElementOrTextVNode = (vNode: VNode): vNode is ElementVNode | TextVNode => { assertDefined(vNode, 'Missing vNode'); const flag = vNode.flags; return (flag & VNodeFlags.ELEMENT_OR_TEXT_MASK) !== 0; @@ -324,22 +314,20 @@ export const vnode_isMaterialized = (vNode: VNode): boolean => { /** @internal */ export const vnode_isTextVNode = (vNode: VNode): vNode is TextVNode => { - assertDefined(vNode, 'Missing vNode'); - const flag = vNode.flags; - return (flag & VNodeFlags.Text) === VNodeFlags.Text; + return vNode instanceof TextVNode; }; /** @internal */ export const vnode_isVirtualVNode = (vNode: VNode): vNode is VirtualVNode => { - assertDefined(vNode, 'Missing vNode'); - const flag = vNode.flags; - return (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual; + return vNode instanceof VirtualVNode; }; export const vnode_isProjection = (vNode: VNode): vNode is VirtualVNode => { assertDefined(vNode, 'Missing vNode'); const flag = vNode.flags; - return (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual && vNode.getProp(QSlot, null) !== null; + return ( + (flag & VNodeFlags.Virtual) === VNodeFlags.Virtual && vnode_getProp(vNode, QSlot, null) !== null + ); }; const ensureTextVNode = (vNode: VNode): TextVNode => { @@ -378,13 +366,56 @@ export const vnode_getNodeTypeName = (vNode: VNode): string => { return ''; }; +export const vnode_getProp = ( + vNode: VNode, + key: string, + getObject: ((id: string) => T) | null +): T | null => { + if (vnode_isElementVNode(vNode) || vnode_isVirtualVNode(vNode)) { + const value = vNode.props?.[key] ?? null; + if (typeof value === 'string' && getObject) { + return getObject(value); + } + return value as T | null; + } + return null; +}; + +export const vnode_setProp = (vNode: VNode, key: string, value: unknown) => { + if (vnode_isElementVNode(vNode) || vnode_isVirtualVNode(vNode)) { + if (value == null && vNode.props) { + delete vNode.props[key]; + } else { + vNode.props ||= {}; + vNode.props[key] = value; + } + } +}; + +export const vnode_setAttr = ( + journal: VNodeJournal, + vNode: VNode, + key: string, + value: string | null | boolean +) => { + if (vnode_isElementVNode(vNode)) { + vnode_setProp(vNode, key, value); + addVNodeOperation(journal, { + operationType: VNodeOperationType.SetAttribute, + target: vNode.node, + attrName: key, + attrValue: value, + }); + } +}; + /** @internal */ export const vnode_ensureElementInflated = (vnode: VNode) => { const flags = vnode.flags; if ((flags & VNodeFlags.INFLATED_TYPE_MASK) === VNodeFlags.Element) { const elementVNode = vnode as ElementVNode; elementVNode.flags ^= VNodeFlags.Inflated; - const element = elementVNode.element; + const element = elementVNode.node; const attributes = element.attributes; for (let idx = 0; idx < attributes.length; idx++) { const attr = attributes[idx]; @@ -394,16 +425,14 @@ export const vnode_ensureElementInflated = (vnode: VNode) => { // all attributes after the ':' are considered immutable, and so we ignore them. break; } else if (key.startsWith(QContainerAttr)) { - const props = vnode_getProps(elementVNode); if (attr.value === QContainerValue.HTML) { - mapArray_set(props, dangerouslySetInnerHTML, element.innerHTML, 0); + vnode_setProp(elementVNode, 'dangerouslySetInnerHTML', element.innerHTML); } else if (attr.value === QContainerValue.TEXT && 'value' in element) { - mapArray_set(props, 'value', element.value, 0); + vnode_setProp(elementVNode, 'value', element.value); } } else if (!key.startsWith(EventNameHtmlScope.on)) { const value = attr.value; - const props = vnode_getProps(elementVNode); - mapArray_set(props, key, value, 0); + vnode_setProp(elementVNode, key, value); } } } @@ -617,48 +646,59 @@ const vnode_ensureTextInflated = (journal: VNodeJournal, vnode: TextVNode) => { if ((flags & VNodeFlags.Inflated) === 0) { const parentNode = vnode_getDomParent(vnode); assertDefined(parentNode, 'Missing parent node.'); - const sharedTextNode = textVNode.textNode as Text; + const sharedTextNode = textVNode.node as Text; const doc = parentNode.ownerDocument; // Walk the previous siblings and inflate them. - let cursor = vnode_getDomSibling(vnode, false, true); + let vCursor = vnode_getDomSibling(vnode, false, true); // If text node is 0 length, than there is no text node. // In that case we use the next node as a reference, in which // case we know that the next node MUST be either NULL or an Element. const node = vnode_getDomSibling(vnode, true, true); const insertBeforeNode: Element | Text | null = sharedTextNode || - (((node instanceof ElementVNode ? node.element : node?.textNode) || null) as - | Element - | Text - | null); + (((node instanceof ElementVNode ? node.node : node?.node) || null) as Element | Text | null); let lastPreviousTextNode = insertBeforeNode; - while (cursor && vnode_isTextVNode(cursor)) { - if ((cursor.flags & VNodeFlags.Inflated) === 0) { - const textNode = doc.createTextNode(cursor.text!); - journal.push(VNodeJournalOpCode.Insert, parentNode, lastPreviousTextNode, textNode); + while (vCursor && vnode_isTextVNode(vCursor)) { + if ((vCursor.flags & VNodeFlags.Inflated) === 0) { + const textNode = doc.createTextNode(vCursor.text!); lastPreviousTextNode = textNode; - cursor.textNode = textNode; - cursor.flags |= VNodeFlags.Inflated; + vCursor.node = textNode; + vCursor.flags |= VNodeFlags.Inflated; + addVNodeOperation(journal, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode, + beforeTarget: lastPreviousTextNode, + target: textNode, + }); } - cursor = vnode_getDomSibling(cursor, false, true); + vCursor = vnode_getDomSibling(vCursor, false, true); } // Walk the next siblings and inflate them. - cursor = vnode; - while (cursor && vnode_isTextVNode(cursor)) { - const next = vnode_getDomSibling(cursor, true, true); + vCursor = vnode; + while (vCursor && vnode_isTextVNode(vCursor)) { + const next = vnode_getDomSibling(vCursor, true, true); const isLastNode = next ? !vnode_isTextVNode(next) : true; - if ((cursor.flags & VNodeFlags.Inflated) === 0) { + if ((vCursor.flags & VNodeFlags.Inflated) === 0) { if (isLastNode && sharedTextNode) { - journal.push(VNodeJournalOpCode.SetText, sharedTextNode, cursor.text!); + addVNodeOperation(journal, { + operationType: VNodeOperationType.SetText, + target: sharedTextNode, + text: vCursor.text!, + }); } else { - const textNode = doc.createTextNode(cursor.text!); - journal.push(VNodeJournalOpCode.Insert, parentNode, insertBeforeNode, textNode); - cursor.textNode = textNode; + const textNode = doc.createTextNode(vCursor.text!); + addVNodeOperation(journal, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode, + beforeTarget: insertBeforeNode, + target: textNode, + }); + vCursor.node = textNode; } - cursor.flags |= VNodeFlags.Inflated; + vCursor.flags |= VNodeFlags.Inflated; } - cursor = next; + vCursor = next; } } }; @@ -666,7 +706,7 @@ const vnode_ensureTextInflated = (journal: VNodeJournal, vnode: TextVNode) => { export const vnode_locate = (rootVNode: ElementVNode, id: string | Element): VNode => { ensureElementVNode(rootVNode); let vNode: VNode | Element = rootVNode; - const containerElement = rootVNode.element as ContainerElement; + const containerElement = rootVNode.node as ContainerElement; const { qVNodeRefs } = containerElement; let elementOffset: number = -1; let refElement: Element | VNode; @@ -751,7 +791,7 @@ export const vnode_getVNodeForChildNode = ( ensureElementVNode(vNode); let child = vnode_getFirstChild(vNode); assertDefined(child, 'Missing child.'); - while (child && (child instanceof ElementVNode ? child.element !== childElement : true)) { + while (child && (child instanceof ElementVNode ? child.node !== childElement : true)) { if (vnode_isVirtualVNode(child)) { const next = child.nextSibling as VNode | null; const firstChild = vnode_getFirstChild(child); @@ -775,7 +815,7 @@ export const vnode_getVNodeForChildNode = ( vNodeStack.pop(); } ensureElementVNode(child); - assertEqual((child as ElementVNode).element, childElement, 'Child not found.'); + assertEqual((child as ElementVNode).node, childElement, 'Child not found.'); // console.log('FOUND', child[VNodeProps.node]?.outerHTML); return child as ElementVNode; }; @@ -793,10 +833,10 @@ const indexOfAlphanumeric = (id: string, length: number): number => { }; export const vnode_createErrorDiv = ( + journal: VNodeJournal, document: Document, host: VNode, - err: Error, - journal: VNodeJournal + err: Error ) => { const errorDiv = document.createElement('errored-host'); if (err && err instanceof Error) { @@ -844,43 +884,30 @@ export const vnode_journalToString = (journal: VNodeJournal): string => { } while (idx < length) { - const op = journal[idx++] as VNodeJournalOpCode; - switch (op) { - case VNodeJournalOpCode.SetText: + const op = journal[idx++]; + switch (op.operationType) { + case VNodeOperationType.SetText: stringify('SetText'); - stringify(' ', journal[idx++]); - stringify(' -->', journal[idx++]); + stringify(' ', op.text); + stringify(' -->', op.target); break; - case VNodeJournalOpCode.SetAttribute: + case VNodeOperationType.SetAttribute: stringify('SetAttribute'); - stringify(' ', journal[idx++]); - stringify(' key', journal[idx++]); - stringify(' val', journal[idx++]); + stringify(' ', op.attrName); + stringify(' key', op.attrName); + stringify(' val', op.attrValue); break; - case VNodeJournalOpCode.HoistStyles: - stringify('HoistStyles'); - break; - case VNodeJournalOpCode.Remove: { - stringify('Remove'); - const parent = journal[idx++]; - stringify(' ', parent); - let nodeToRemove: any; - while (idx < length && typeof (nodeToRemove = journal[idx]) !== 'number') { - stringify(' -->', nodeToRemove); - idx++; - } + case VNodeOperationType.Delete: { + stringify('Delete'); + stringify(' -->', op.target); break; } - case VNodeJournalOpCode.Insert: { - stringify('Insert'); - const parent = journal[idx++]; - const insertBefore = journal[idx++]; + case VNodeOperationType.InsertOrMove: { + stringify('InsertOrMove'); + const parent = op.parent; + const insertBefore = op.beforeTarget; stringify(' ', parent); - let newChild: any; - while (idx < length && typeof (newChild = journal[idx]) !== 'number') { - stringify(' -->', newChild); - idx++; - } + stringify(' -->', op.target); if (insertBefore) { stringify(' ', insertBefore); } @@ -891,113 +918,7 @@ export const vnode_journalToString = (journal: VNodeJournal): string => { lines.push('END JOURNAL'); return lines.join('\n'); }; - -const parseBoolean = (value: string | boolean | null): boolean => { - if (value === 'false') { - return false; - } - return Boolean(value); -}; - -const isBooleanAttr = (element: Element, key: string): boolean => { - const isBoolean = - key == 'allowfullscreen' || - key == 'async' || - key == 'autofocus' || - key == 'autoplay' || - key == 'checked' || - key == 'controls' || - key == 'default' || - key == 'defer' || - key == 'disabled' || - key == 'formnovalidate' || - key == 'inert' || - key == 'ismap' || - key == 'itemscope' || - key == 'loop' || - key == 'multiple' || - key == 'muted' || - key == 'nomodule' || - key == 'novalidate' || - key == 'open' || - key == 'playsinline' || - key == 'readonly' || - key == 'required' || - key == 'reversed' || - key == 'selected'; - return isBoolean && key in element; -}; - -export const vnode_applyJournal = (journal: VNodeJournal) => { - // console.log('APPLY JOURNAL', vnode_journalToString(journal)); - let idx = 0; - const length = journal.length; - while (idx < length) { - const op = journal[idx++] as VNodeJournalOpCode; - switch (op) { - case VNodeJournalOpCode.SetText: - const text = journal[idx++] as Text; - text.nodeValue = journal[idx++] as string; - break; - case VNodeJournalOpCode.SetAttribute: - const element = journal[idx++] as Element; - let key = journal[idx++] as string; - if (key === 'className') { - key = 'class'; - } - const value = journal[idx++] as string | null | boolean; - const shouldRemove = value == null || value === false; - if (isBooleanAttr(element, key)) { - (element as any)[key] = parseBoolean(value); - } else if (key === dangerouslySetInnerHTML) { - (element as any).innerHTML = value!; - element.setAttribute(QContainerAttr, QContainerValue.HTML); - } else if (shouldRemove) { - element.removeAttribute(key); - } else if (key === 'value' && key in element) { - (element as any).value = String(value); - } else { - element.setAttribute(key, String(value)); - } - break; - case VNodeJournalOpCode.HoistStyles: - const document = journal[idx++] as Document; - const head = document.head; - const styles = document.querySelectorAll(QStylesAllSelector); - for (let i = 0; i < styles.length; i++) { - head.appendChild(styles[i]); - } - break; - case VNodeJournalOpCode.Remove: - const removeParent = journal[idx++] as Element; - let nodeToRemove: any; - while (idx < length && typeof (nodeToRemove = journal[idx]) !== 'number') { - removeParent.removeChild(nodeToRemove as Element | Text); - idx++; - } - break; - case VNodeJournalOpCode.RemoveAll: - const removeAllParent = journal[idx++] as Element; - if (removeAllParent.replaceChildren) { - removeAllParent.replaceChildren(); - } else { - // fallback if replaceChildren is not supported - removeAllParent.textContent = ''; - } - break; - case VNodeJournalOpCode.Insert: - const insertParent = journal[idx++] as Element; - const insertBefore = journal[idx++] as Element | Text | null; - let newChild: any; - while (idx < length && typeof (newChild = journal[idx]) !== 'number') { - insertParent.insertBefore(newChild, insertBefore); - idx++; - } - break; - } - } - journal.length = 0; -}; +export const vnode_applyJournal = _flushJournal; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1011,7 +932,7 @@ export const vnode_insertBefore = ( if (vnode_isElementVNode(parent)) { ensureMaterialized(parent); } - const newChildCurrentParent = newChild.parent; + const newChildCurrentParent = newChild.parent as ElementVNode | VirtualVNode | null; if (newChild === insertBefore) { // invalid insertBefore. We can't insert before self reference // prevent infinity loop and putting self reference to next sibling @@ -1045,8 +966,8 @@ export const vnode_insertBefore = ( * find children first (and inflate them). */ const domParentVNode = vnode_getDomParentVNode(parent, false); - const parentNode = domParentVNode && domParentVNode.element; - let domChildren: (Element | Text)[] | null = null; + const parentNode = domParentVNode && domParentVNode.node; + let domChildren: (ElementVNode | TextVNode)[] | null = null; if (domParentVNode) { domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert( journal, @@ -1101,9 +1022,9 @@ export const vnode_insertBefore = ( const parentIsDeleted = parent.flags & VNodeFlags.Deleted; + let adjustedInsertBefore: VNode | null = null; // if the parent is deleted, then we don't need to insert the new child if (!parentIsDeleted) { - let adjustedInsertBefore: VNode | null = null; if (insertBefore == null) { if (vnode_isVirtualVNode(parent)) { // If `insertBefore` is null, than we need to insert at the end of the list. @@ -1120,14 +1041,15 @@ export const vnode_insertBefore = ( } adjustedInsertBefore && vnode_ensureInflatedIfText(journal, adjustedInsertBefore); - // Here we know the insertBefore node if (domChildren && domChildren.length) { - journal.push( - VNodeJournalOpCode.Insert, - parentNode, - vnode_getNode(adjustedInsertBefore), - ...domChildren - ); + for (const child of domChildren) { + addVNodeOperation(journal, { + operationType: VNodeOperationType.InsertOrMove, + parent: parentNode!, + beforeTarget: vnode_getNode(adjustedInsertBefore), + target: child.node!, + }); + } } } @@ -1153,12 +1075,9 @@ export const vnode_insertBefore = ( } }; -export const vnode_getDomParent = ( - vnode: VNode, - includeProjection = true -): Element | Text | null => { +export const vnode_getDomParent = (vnode: VNode, includeProjection = true): Element | null => { vnode = vnode_getDomParentVNode(vnode, includeProjection) as VNode; - return (vnode && (vnode as ElementVNode).element) as Element | Text | null; + return (vnode && (vnode as ElementVNode).node) as Element | null; }; export const vnode_getDomParentVNode = ( @@ -1184,13 +1103,21 @@ export const vnode_remove = ( if (removeDOM) { const domParent = vnode_getDomParent(vParent, false); - const isInnerHTMLParent = vParent.getAttr(dangerouslySetInnerHTML); + const isInnerHTMLParent = vnode_getProp(vParent, dangerouslySetInnerHTML, null) !== null; if (isInnerHTMLParent) { // ignore children, as they are inserted via innerHTML return; } - const children = vnode_getDOMChildNodes(journal, vToRemove); - domParent && children.length && journal.push(VNodeJournalOpCode.Remove, domParent, ...children); + const children = vnode_getDOMChildNodes(journal, vToRemove, true); + //&& //journal.push(VNodeOperationType.Remove, domParent, ...children); + if (domParent && children.length) { + for (const child of children) { + addVNodeOperation(journal, { + operationType: VNodeOperationType.Delete, + target: child.node!, + }); + } + } } const vPrevious = vToRemove.previousSibling; @@ -1210,6 +1137,8 @@ export const vnode_remove = ( }; export const vnode_queryDomNodes = ( + container: Container, + journal: VNodeJournal, vNode: VNode, selector: string, cb: (element: Element) => void @@ -1224,13 +1153,14 @@ export const vnode_queryDomNodes = ( } else { let child = vnode_getFirstChild(vNode); while (child) { - vnode_queryDomNodes(child, selector, cb); + vnode_queryDomNodes(container, journal, child, selector, cb); child = child.nextSibling as VNode | null; } } }; export const vnode_truncate = ( + container: Container, journal: VNodeJournal, vParent: ElementVNode | VirtualVNode, vDelete: VNode @@ -1239,10 +1169,20 @@ export const vnode_truncate = ( const parent = vnode_getDomParent(vParent); if (parent) { if (vnode_isElementVNode(vParent)) { - journal.push(VNodeJournalOpCode.RemoveAll, parent); + addVNodeOperation(journal, { + operationType: VNodeOperationType.RemoveAllChildren, + target: vParent.node!, + }); } else { - const children = vnode_getDOMChildNodes(journal, vParent); - children.length && journal.push(VNodeJournalOpCode.Remove, parent, ...children); + const children = vnode_getDOMChildNodes(journal, vParent, true); + if (children.length) { + for (const child of children) { + addVNodeOperation(journal, { + operationType: VNodeOperationType.Delete, + target: child.node!, + }); + } + } } } const vPrevious = vDelete.previousSibling; @@ -1260,7 +1200,7 @@ export const vnode_getElementName = (vnode: ElementVNode): string => { const elementVNode = ensureElementVNode(vnode); let elementName = elementVNode.elementName; if (elementName === undefined) { - const element = elementVNode.element; + const element = elementVNode.node; const nodeName = fastNodeName(element)!.toLowerCase(); elementName = elementVNode.elementName = nodeName; elementVNode.flags |= vnode_getElementNamespaceFlags(element); @@ -1271,15 +1211,19 @@ export const vnode_getElementName = (vnode: ElementVNode): string => { export const vnode_getText = (textVNode: TextVNode): string => { let text = textVNode.text; if (text === undefined) { - text = textVNode.text = textVNode.textNode!.nodeValue!; + text = textVNode.text = textVNode.node!.nodeValue!; } return text; }; export const vnode_setText = (journal: VNodeJournal, textVNode: TextVNode, text: string) => { vnode_ensureTextInflated(journal, textVNode); - const textNode = textVNode.textNode!; - journal.push(VNodeJournalOpCode.SetText, textNode, (textVNode.text = text)); + textVNode.text = text; + addVNodeOperation(journal, { + operationType: VNodeOperationType.SetText, + target: textVNode.node!, + text: text, + }); }; /** @internal */ @@ -1295,7 +1239,7 @@ export const vnode_getFirstChild = (vnode: VNode): VNode | null => { }; const vnode_materialize = (vNode: ElementVNode) => { - const element = vNode.element; + const element = vNode.node; const firstChild = fastFirstChild(element); const vNodeData = (element.ownerDocument as QDocument)?.qVNodeData?.get(element); @@ -1354,7 +1298,7 @@ export const ensureMaterialized = (vnode: ElementVNode): VNode | null => { let vFirstChild = vParent.firstChild; if (vFirstChild === undefined) { // need to materialize the vNode. - const element = vParent.element; + const element = vParent.node; if (vParent.parent && shouldIgnoreChildren(element)) { // We have a container with html value, must ignore the content. @@ -1555,14 +1499,14 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null, vDat processVNodeData(vData, (peek, consumeValue) => { if (peek() === VNodeDataChar.ID) { if (!container) { - container = getDomContainer(vParent.element); + container = getDomContainer(vParent.node); } const id = consumeValue(); container.$setRawState$(parseInt(id), vParent); - isDev && vParent.setAttr(ELEMENT_ID, id, null); + isDev && vnode_setProp(vParent, ELEMENT_ID, id); } else if (peek() === VNodeDataChar.BACK_REFS) { if (!container) { - container = getDomContainer(vParent.element); + container = getDomContainer(vParent.node); } setEffectBackRefFromVNodeData(vParent, consumeValue(), container); } else { @@ -1671,11 +1615,14 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[] if ((type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) !== 0) { vnode_ensureElementInflated(vnode); const keys: string[] = []; - const props = vnode_getProps(vnode); - for (let i = 0; i < props.length; i = i + 2) { - const key = props[i] as string; - if (!key.startsWith(Q_PROPS_SEPARATOR)) { - keys.push(key); + const props = vnode.props; + if (props) { + const keys = Object.keys(props); + for (let i = 0; i < Math.min(keys.length, 20); i++) { + const key = keys[i]; + if (!key.startsWith(Q_PROPS_SEPARATOR)) { + keys.push(key); + } } } return keys; @@ -1683,12 +1630,6 @@ export const vnode_getAttrKeys = (vnode: ElementVNode | VirtualVNode): string[] return []; }; -/** @internal */ -export const vnode_getProps = (vnode: ElementVNode | VirtualVNode): unknown[] => { - vnode.props ||= []; - return vnode.props; -}; - export const vnode_isDescendantOf = (vnode: VNode, ancestor: VNode): boolean => { let parent: VNode | null = vnode_getProjectionParentOrParent(vnode); while (parent) { @@ -1708,11 +1649,7 @@ export const vnode_getNode = (vnode: VNode | null): Element | Text | null => { if (vnode === null || vnode_isVirtualVNode(vnode)) { return null; } - if (vnode_isElementVNode(vnode)) { - return vnode.element; - } - assertTrue(vnode_isTextVNode(vnode), 'Expecting Text Node.'); - return (vnode as TextVNode).textNode!; + return (vnode as ElementVNode | TextVNode).node; }; /** @internal */ @@ -1745,13 +1682,13 @@ export function vnode_toString( const attrs: string[] = ['[' + String(idx) + ']']; vnode_getAttrKeys(vnode).forEach((key) => { if (key !== DEBUG_TYPE) { - const value = vnode!.getAttr(key); + const value = vnode_getProp(vnode!, key, null); attrs.push(' ' + key + '=' + qwikDebugToString(value)); } }); const name = (colorize ? NAME_COL_PREFIX : '') + - (VirtualTypeName[vnode.getAttr(DEBUG_TYPE) || VirtualType.Virtual] || + (VirtualTypeName[vnode_getProp(vnode, DEBUG_TYPE, null) || VirtualType.Virtual] || VirtualTypeName[VirtualType.Virtual]) + (colorize ? NAME_COL_SUFFIX : ''); strings.push('<' + name + attrs.join('') + '>'); @@ -1764,9 +1701,18 @@ export function vnode_toString( } else if (vnode_isElementVNode(vnode)) { const tag = vnode_getElementName(vnode); const attrs: string[] = []; + if (isCursor(vnode)) { + attrs.push(' cursor'); + } + if (vnode.dirty) { + attrs.push(` dirty:${vnode.dirty}`); + } + if (vnode.dirtyChildren) { + attrs.push(` dirtyChildren[${vnode.dirtyChildren.length}]`); + } const keys = vnode_getAttrKeys(vnode); keys.forEach((key) => { - const value = vnode!.getAttr(key); + const value = vnode_getProp(vnode!, key, null); attrs.push(' ' + key + '=' + qwikDebugToString(value)); }); const node = vnode_getNode(vnode) as HTMLElement; @@ -1869,18 +1815,18 @@ function materializeFromVNodeData( } // collect the elements; } else if (peek() === VNodeDataChar.SCOPED_STYLE) { - vParent.setAttr(QScopedStyle, consumeValue(), null); + vnode_setProp(vParent, QScopedStyle, consumeValue()); } else if (peek() === VNodeDataChar.RENDER_FN) { - vParent.setAttr(OnRenderProp, consumeValue(), null); + vnode_setProp(vParent, OnRenderProp, consumeValue()); } else if (peek() === VNodeDataChar.ID) { if (!container) { container = getDomContainer(element); } const id = consumeValue(); container.$setRawState$(parseInt(id), vParent); - isDev && vParent.setAttr(ELEMENT_ID, id, null); + isDev && vnode_setProp(vParent, ELEMENT_ID, id); } else if (peek() === VNodeDataChar.PROPS) { - vParent.setAttr(ELEMENT_PROPS, consumeValue(), null); + vnode_setProp(vParent, ELEMENT_PROPS, consumeValue()); } else if (peek() === VNodeDataChar.KEY) { const isEscapedValue = getChar(nextToConsumeIdx + 1) === VNodeDataChar.SEPARATOR; let value; @@ -1891,11 +1837,11 @@ function materializeFromVNodeData( } else { value = consumeValue(); } - vParent.setAttr(ELEMENT_KEY, value, null); + vnode_setProp(vParent, ELEMENT_KEY, value); } else if (peek() === VNodeDataChar.SEQ) { - vParent.setAttr(ELEMENT_SEQ, consumeValue(), null); + vnode_setProp(vParent, ELEMENT_SEQ, consumeValue()); } else if (peek() === VNodeDataChar.SEQ_IDX) { - vParent.setAttr(ELEMENT_SEQ_IDX, consumeValue(), null); + vnode_setProp(vParent, ELEMENT_SEQ_IDX, consumeValue()); } else if (peek() === VNodeDataChar.BACK_REFS) { if (!container) { container = getDomContainer(element); @@ -1907,7 +1853,7 @@ function materializeFromVNodeData( } vParent.slotParent = vnode_locate(container!.rootVNode, consumeValue()); } else if (peek() === VNodeDataChar.CONTEXT) { - vParent.setAttr(QCtxAttr, consumeValue(), null); + vnode_setProp(vParent, QCtxAttr, consumeValue()); } else if (peek() === VNodeDataChar.OPEN) { consume(); addVNode(vnode_newVirtual()); @@ -1918,7 +1864,7 @@ function materializeFromVNodeData( } else if (peek() === VNodeDataChar.SEPARATOR) { const key = consumeValue(); const value = consumeValue(); - vParent.setAttr(key, value, null); + vnode_setProp(vParent, key, value); } else if (peek() === VNodeDataChar.CLOSE) { consume(); vParent.lastChild = vLast; @@ -1928,7 +1874,7 @@ function materializeFromVNodeData( vFirst = stack.pop(); vParent = stack.pop(); } else if (peek() === VNodeDataChar.SLOT) { - vParent.setAttr(QSlot, consumeValue(), null); + vnode_setProp(vParent, QSlot, consumeValue()); } else { // skip over style or non-qwik elements in front of text nodes, where text node is the first child (except the style node) while (isElement(child) && shouldSkipElement(child)) { @@ -2001,7 +1947,7 @@ export const vnode_getProjectionParentComponent = (vHost: VNode): VirtualVNode | while (projectionDepth--) { while ( vHost && - (vnode_isVirtualVNode(vHost) ? vHost.getProp(OnRenderProp, null) === null : true) + (vnode_isVirtualVNode(vHost) ? vnode_getProp(vHost, OnRenderProp, null) === null : true) ) { const qSlotParent = vHost.slotParent; const vProjectionParent = vnode_isVirtualVNode(vHost) && qSlotParent; diff --git a/packages/qwik/src/core/debug.ts b/packages/qwik/src/core/debug.ts index 5645370d4c4..6853f300e31 100644 --- a/packages/qwik/src/core/debug.ts +++ b/packages/qwik/src/core/debug.ts @@ -1,6 +1,6 @@ import { isSignal } from './reactive-primitives/utils'; // ^ keep this first to avoid circular dependency breaking class extend -import { vnode_isVNode } from './client/vnode'; +import { vnode_getProp, vnode_isVNode } from './client/vnode'; import { ComputedSignalImpl } from './reactive-primitives/impl/computed-signal-impl'; import { isStore } from './reactive-primitives/impl/store'; import { WrappedSignalImpl } from './reactive-primitives/impl/wrapped-signal-impl'; @@ -11,51 +11,56 @@ import { isTask } from './use/use-task'; const stringifyPath: any[] = []; export function qwikDebugToString(value: any): any { - if (value === null) { - return 'null'; - } else if (value === undefined) { - return 'undefined'; - } else if (typeof value === 'string') { - return '"' + value + '"'; - } else if (typeof value === 'number' || typeof value === 'boolean') { - return String(value); - } else if (isTask(value)) { - return `Task(${qwikDebugToString(value.$qrl$)})`; - } else if (isQrl(value)) { - return `Qrl(${value.$symbol$})`; - } else if (typeof value === 'object' || typeof value === 'function') { - if (stringifyPath.includes(value)) { - return '*'; - } - if (stringifyPath.length > 10) { - // debugger; - } - try { - stringifyPath.push(value); - if (Array.isArray(value)) { - if (vnode_isVNode(value)) { - return '(' + value.getProp(DEBUG_TYPE, null) + ')'; - } else { - return value.map(qwikDebugToString); - } - } else if (isSignal(value)) { - if (value instanceof WrappedSignalImpl) { - return 'WrappedSignal'; - } else if (value instanceof ComputedSignalImpl) { - return 'ComputedSignal'; - } else { - return 'Signal'; + try { + if (value === null) { + return 'null'; + } else if (value === undefined) { + return 'undefined'; + } else if (typeof value === 'string') { + return '"' + value + '"'; + } else if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } else if (isTask(value)) { + return `Task(${qwikDebugToString(value.$qrl$)})`; + } else if (isQrl(value)) { + return `Qrl(${value.$symbol$})`; + } else if (typeof value === 'object' || typeof value === 'function') { + if (stringifyPath.includes(value)) { + return '*'; + } + if (stringifyPath.length > 10) { + // debugger; + } + try { + stringifyPath.push(value); + if (Array.isArray(value)) { + if (vnode_isVNode(value)) { + return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')'; + } else { + return value.map(qwikDebugToString); + } + } else if (isSignal(value)) { + if (value instanceof WrappedSignalImpl) { + return 'WrappedSignal'; + } else if (value instanceof ComputedSignalImpl) { + return 'ComputedSignal'; + } else { + return 'Signal'; + } + } else if (isStore(value)) { + return 'Store'; + } else if (isJSXNode(value)) { + return jsxToString(value); + } else if (vnode_isVNode(value)) { + return '(' + (vnode_getProp(value, DEBUG_TYPE, null) || 'vnode') + ')'; } - } else if (isStore(value)) { - return 'Store'; - } else if (isJSXNode(value)) { - return jsxToString(value); - } else if (vnode_isVNode(value)) { - return '(' + value.getProp(DEBUG_TYPE, null) + ')'; + } finally { + stringifyPath.pop(); } - } finally { - stringifyPath.pop(); } + } catch (e) { + console.error('ERROR in qwikDebugToString', e); + return '*error*'; } return value; } diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 317dff3543d..c17d6941e52 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -20,23 +20,20 @@ export { vnode_ensureElementInflated as _vnode_ensureElementInflated, vnode_getAttrKeys as _vnode_getAttrKeys, vnode_getFirstChild as _vnode_getFirstChild, - vnode_getProps as _vnode_getProps, vnode_isMaterialized as _vnode_isMaterialized, vnode_isTextVNode as _vnode_isTextVNode, vnode_isVirtualVNode as _vnode_isVirtualVNode, vnode_toString as _vnode_toString, } from './client/vnode'; -export type { - ElementVNode as _ElementVNode, - TextVNode as _TextVNode, - VirtualVNode as _VirtualVNode, - VNode as _VNode, -} from './client/vnode-impl'; +export type { VNode as _VNode } from './shared/vnode/vnode'; +export type { ElementVNode as _ElementVNode } from './shared/vnode/element-vnode'; +export type { TextVNode as _TextVNode } from './shared/vnode/text-vnode'; +export type { VirtualVNode as _VirtualVNode } from './shared/vnode/virtual-vnode'; export { _hasStoreEffects, isStore as _isStore } from './reactive-primitives/impl/store'; export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api'; export { SubscriptionData as _SubscriptionData } from './reactive-primitives/subscription-data'; -export { _EFFECT_BACK_REF } from './reactive-primitives/types'; +export { _EFFECT_BACK_REF } from './reactive-primitives/backref'; export { isStringifiable as _isStringifiable, type Stringifiable as _Stringifiable, diff --git a/packages/qwik/src/core/reactive-primitives/backref.ts b/packages/qwik/src/core/reactive-primitives/backref.ts new file mode 100644 index 00000000000..ee9a49d0778 --- /dev/null +++ b/packages/qwik/src/core/reactive-primitives/backref.ts @@ -0,0 +1,7 @@ +/** @internal */ +export const _EFFECT_BACK_REF = Symbol('backRef'); + +/** Class for back reference to the EffectSubscription */ +export abstract class BackRef { + [_EFFECT_BACK_REF]: Map | undefined = undefined; +} diff --git a/packages/qwik/src/core/reactive-primitives/cleanup.ts b/packages/qwik/src/core/reactive-primitives/cleanup.ts index b8a192e76ea..f69bd6b434c 100644 --- a/packages/qwik/src/core/reactive-primitives/cleanup.ts +++ b/packages/qwik/src/core/reactive-primitives/cleanup.ts @@ -3,21 +3,11 @@ import type { Container } from '../shared/types'; import { SignalImpl } from './impl/signal-impl'; import { WrappedSignalImpl } from './impl/wrapped-signal-impl'; import { StoreHandler, getStoreHandler } from './impl/store'; -import { - EffectSubscriptionProp, - _EFFECT_BACK_REF, - type Consumer, - type EffectProperty, - type EffectSubscription, -} from './types'; import { AsyncComputedSignalImpl } from './impl/async-computed-signal-impl'; -import { isPropsProxy, type PropsProxyHandler } from '../shared/jsx/props-proxy'; import { _PROPS_HANDLER } from '../shared/utils/constants'; - -/** Class for back reference to the EffectSubscription */ -export abstract class BackRef { - [_EFFECT_BACK_REF]: Map | undefined = undefined; -} +import { BackRef, _EFFECT_BACK_REF } from './backref'; +import { EffectSubscriptionProp, type Consumer, type EffectSubscription } from './types'; +import { isPropsProxy, type PropsProxyHandler } from '../shared/jsx/props-proxy'; export function clearAllEffects(container: Container, consumer: Consumer): void { if (vnode_isVNode(consumer) && vnode_isElementVNode(consumer)) { diff --git a/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts index 46b6e694af3..4ad842d5fcd 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/async-computed-signal-impl.ts @@ -1,13 +1,11 @@ import { qwikDebugToString } from '../../debug'; import type { NoSerialize } from '../../shared/serdes/verify'; import type { Container } from '../../shared/types'; -import { ChoreType } from '../../shared/util-chore-type'; import { isPromise, retryOnPromise } from '../../shared/utils/promises'; import { cleanupDestroyable } from '../../use/utils/destroyable'; import { cleanupFn, trackFn } from '../../use/utils/tracker'; -import type { BackRef } from '../cleanup'; +import { _EFFECT_BACK_REF, type BackRef } from '../backref'; import { - _EFFECT_BACK_REF, AsyncComputeQRL, EffectProperty, EffectSubscription, @@ -69,12 +67,7 @@ export class AsyncComputedSignalImpl set untrackedLoading(value: boolean) { if (value !== this.$untrackedLoading$) { this.$untrackedLoading$ = value; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - this.$loadingEffects$ - ); + scheduleEffects(this.$container$, this, this.$loadingEffects$); } } @@ -94,12 +87,7 @@ export class AsyncComputedSignalImpl set untrackedError(value: Error | undefined) { if (value !== this.$untrackedError$) { this.$untrackedError$ = value; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - this.$errorEffects$ - ); + scheduleEffects(this.$container$, this, this.$errorEffects$); } } diff --git a/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts index cd29db54ad2..c27c05a0796 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/computed-signal-impl.ts @@ -2,16 +2,15 @@ import { qwikDebugToString } from '../../debug'; import { assertFalse } from '../../shared/error/assert'; import { QError, qError } from '../../shared/error/error'; import type { Container } from '../../shared/types'; -import { ChoreType } from '../../shared/util-chore-type'; import { isPromise } from '../../shared/utils/promises'; import { tryGetInvokeContext } from '../../use/use-core'; -import { throwIfQRLNotResolved } from '../utils'; -import type { BackRef } from '../cleanup'; +import { scheduleEffects, throwIfQRLNotResolved } from '../utils'; import { getSubscriber } from '../subscriber'; import { SerializationSignalFlags, ComputeQRL, EffectSubscription } from '../types'; -import { _EFFECT_BACK_REF, EffectProperty, NEEDS_COMPUTATION, SignalFlags } from '../types'; +import { EffectProperty, NEEDS_COMPUTATION, SignalFlags } from '../types'; import { SignalImpl } from './signal-impl'; import type { QRLInternal } from '../../shared/qrl/qrl-class'; +import { _EFFECT_BACK_REF, type BackRef } from '../backref'; const DEBUG = false; // eslint-disable-next-line no-console @@ -53,12 +52,7 @@ export class ComputedSignalImpl> invalidate() { this.$flags$ |= SignalFlags.INVALID; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - this.$effects$ - ); + scheduleEffects(this.$container$, this, this.$effects$); } /** diff --git a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts index 3c59a0cb242..b47b10574c2 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/signal-impl.ts @@ -9,10 +9,10 @@ import { addQrlToSerializationCtx, ensureContainsBackRef, ensureContainsSubscription, + scheduleEffects, } from '../utils'; import type { Signal } from '../signal.public'; import { SignalFlags, type EffectSubscription } from '../types'; -import { ChoreType } from '../../shared/util-chore-type'; import type { WrappedSignalImpl } from './wrapped-signal-impl'; const DEBUG = false; @@ -38,12 +38,7 @@ export class SignalImpl implements Signal { * remained the same object */ force() { - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - this.$effects$ - ); + scheduleEffects(this.$container$, this, this.$effects$); } get untrackedValue() { @@ -68,12 +63,7 @@ export class SignalImpl implements Signal { DEBUG && log('Signal.set', this.$untrackedValue$, '->', value, pad('\n' + this.toString(), ' ')); this.$untrackedValue$ = value; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - this.$effects$ - ); + scheduleEffects(this.$container$, this, this.$effects$); } } diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.ts b/packages/qwik/src/core/reactive-primitives/impl/store.ts index 391f3a9d933..2e6903ff514 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/store.ts @@ -7,6 +7,7 @@ import { addQrlToSerializationCtx, ensureContainsBackRef, ensureContainsSubscription, + scheduleEffects, } from '../utils'; import { STORE_ALL_PROPS, @@ -16,7 +17,6 @@ import { type EffectSubscription, type StoreTarget, } from '../types'; -import { ChoreType } from '../../shared/util-chore-type'; import type { PropsProxy, PropsProxyHandler } from '../../shared/jsx/props-proxy'; const DEBUG = false; @@ -109,12 +109,7 @@ export class StoreHandler implements ProxyHandler { force(prop: keyof StoreTarget): void { const target = getStoreTarget(this)!; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - getEffects(target, prop, this.$effects$) - ); + scheduleEffects(this.$container$, this, getEffects(target, prop, this.$effects$)); } get(target: StoreTarget, prop: string | symbol) { @@ -199,12 +194,7 @@ export class StoreHandler implements ProxyHandler { if (!Array.isArray(target)) { // If the target is an array, we don't need to trigger effects. // Changing the length property will trigger effects. - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - this, - getEffects(target, prop, this.$effects$) - ); + scheduleEffects(this.$container$, this, getEffects(target, prop, this.$effects$)); } return true; } @@ -292,12 +282,7 @@ function setNewValueAndTriggerEffects>( (target as any)[prop] = value; const effects = getEffects(target, prop, currentStore.$effects$); if (effects) { - currentStore.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - currentStore, - effects - ); + scheduleEffects(currentStore.$container$, currentStore, effects); } } diff --git a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts index 164ffdc39fc..d7a1de5b5ad 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/wrapped-signal-impl.ts @@ -1,20 +1,17 @@ +import { vnode_setProp } from '../../client/vnode'; import { assertFalse } from '../../shared/error/assert'; import { QError, qError } from '../../shared/error/error'; import type { Container, HostElement } from '../../shared/types'; -import { ChoreType } from '../../shared/util-chore-type'; +import { HOST_SIGNAL } from '../../shared/utils/markers'; +import { ChoreBits } from '../../shared/vnode/enums/chore-bits.enum'; import { trackSignal } from '../../use/use-core'; -import type { BackRef } from '../cleanup'; import { getValueProp } from '../internal-api'; import type { AllSignalFlags, EffectSubscription } from '../types'; -import { - _EFFECT_BACK_REF, - EffectProperty, - NEEDS_COMPUTATION, - SignalFlags, - WrappedSignalFlags, -} from '../types'; +import { EffectProperty, NEEDS_COMPUTATION, SignalFlags, WrappedSignalFlags } from '../types'; import { isSignal, scheduleEffects } from '../utils'; import { SignalImpl } from './signal-impl'; +import { markVNodeDirty } from '../../shared/vnode/vnode-dirty'; +import { _EFFECT_BACK_REF, type BackRef } from '../backref'; export class WrappedSignalImpl extends SignalImpl implements BackRef { $args$: any[]; @@ -48,12 +45,10 @@ export class WrappedSignalImpl extends SignalImpl implements BackRef { try { this.$computeIfNeeded$(); } catch (_) { - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - this.$hostElement$, - this, - this.$effects$ - ); + if (this.$container$ && this.$hostElement$) { + vnode_setProp(this.$hostElement$, HOST_SIGNAL, this); + markVNodeDirty(this.$container$, this.$hostElement$, ChoreBits.COMPUTE); + } } // if the computation not failed, we can run the effects directly if (this.$flags$ & SignalFlags.RUN_EFFECTS) { @@ -68,12 +63,10 @@ export class WrappedSignalImpl extends SignalImpl implements BackRef { */ force() { this.$flags$ |= SignalFlags.RUN_EFFECTS; - this.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - this.$hostElement$, - this, - this.$effects$ - ); + if (this.$container$ && this.$hostElement$) { + vnode_setProp(this.$hostElement$, HOST_SIGNAL, this); + markVNodeDirty(this.$container$, this.$hostElement$, ChoreBits.COMPUTE); + } } get untrackedValue() { diff --git a/packages/qwik/src/core/reactive-primitives/subscriber.ts b/packages/qwik/src/core/reactive-primitives/subscriber.ts index d46ef1223d8..47a60acd2c7 100644 --- a/packages/qwik/src/core/reactive-primitives/subscriber.ts +++ b/packages/qwik/src/core/reactive-primitives/subscriber.ts @@ -1,9 +1,9 @@ import { isServer } from '@qwik.dev/core/build'; import { QBackRefs } from '../shared/utils/markers'; import type { ISsrNode } from '../ssr/ssr-types'; -import { BackRef } from './cleanup'; import type { Consumer, EffectProperty, EffectSubscription } from './types'; -import { _EFFECT_BACK_REF, EffectSubscriptionProp } from './types'; +import { EffectSubscriptionProp } from './types'; +import { _EFFECT_BACK_REF, type BackRef } from './backref'; export function getSubscriber( effect: Consumer, diff --git a/packages/qwik/src/core/reactive-primitives/subscription-data.ts b/packages/qwik/src/core/reactive-primitives/subscription-data.ts index 7d55126d3ac..da834e8d523 100644 --- a/packages/qwik/src/core/reactive-primitives/subscription-data.ts +++ b/packages/qwik/src/core/reactive-primitives/subscription-data.ts @@ -17,3 +17,9 @@ export class SubscriptionData { this.data = data; } } + +export interface NodeProp { + isConst: boolean; + scopedStyleIdPrefix: string | null; + value: Signal | string; +} diff --git a/packages/qwik/src/core/reactive-primitives/types.ts b/packages/qwik/src/core/reactive-primitives/types.ts index c874d17377e..f62a93fab14 100644 --- a/packages/qwik/src/core/reactive-primitives/types.ts +++ b/packages/qwik/src/core/reactive-primitives/types.ts @@ -1,4 +1,3 @@ -import type { ISsrNode } from '../ssr/ssr-types'; import type { Task, Tracker } from '../use/use-task'; import type { SubscriptionData } from './subscription-data'; import type { ReadonlySignal } from './signal.public'; @@ -8,7 +7,7 @@ import type { SerializerSymbol } from '../shared/serdes/verify'; import type { ComputedFn } from '../use/use-computed'; import type { AsyncComputedFn } from '../use/use-async-computed'; import type { Container, SerializationStrategy } from '../shared/types'; -import type { VNode } from '../client/vnode-impl'; +import type { VNode } from '../shared/vnode/vnode'; /** * # ================================ @@ -24,9 +23,6 @@ import type { VNode } from '../client/vnode-impl'; */ export const NEEDS_COMPUTATION: any = Symbol('invalid'); -/** @internal */ -export const _EFFECT_BACK_REF = Symbol('backRef'); - export interface InternalReadonlySignal extends ReadonlySignal { readonly untrackedValue: T; } @@ -77,7 +73,7 @@ export type AllSignalFlags = SignalFlags | WrappedSignalFlags | SerializationSig * - `VNode` and `ISsrNode`: Either a component or `` * - `Signal2`: A derived signal which contains a computation function. */ -export type Consumer = Task | VNode | ISsrNode | SignalImpl; +export type Consumer = Task | VNode | SignalImpl; /** * An effect consumer plus type of effect, back references to producers and additional data diff --git a/packages/qwik/src/core/reactive-primitives/utils.ts b/packages/qwik/src/core/reactive-primitives/utils.ts index 1e90d186af7..a7593b98308 100644 --- a/packages/qwik/src/core/reactive-primitives/utils.ts +++ b/packages/qwik/src/core/reactive-primitives/utils.ts @@ -1,23 +1,19 @@ import { isDomContainer } from '../client/dom-container'; -import { pad, qwikDebugToString } from '../debug'; -import type { OnRenderFn } from '../shared/component.public'; +import { qwikDebugToString } from '../debug'; import { assertDefined } from '../shared/error/assert'; -import type { Props } from '../shared/jsx/jsx-runtime'; import { isServerPlatform } from '../shared/platform/platform'; -import { type QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; -import type { Container, HostElement, SerializationStrategy } from '../shared/types'; -import { ChoreType } from '../shared/util-chore-type'; -import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers'; +import type { Container, SerializationStrategy } from '../shared/types'; +import { OnRenderProp } from '../shared/utils/markers'; import { SerializerSymbol } from '../shared/serdes/verify'; import { isObject } from '../shared/utils/types'; -import type { ISsrNode, SSRContainer } from '../ssr/ssr-types'; +import type { SSRContainer } from '../ssr/ssr-types'; import { TaskFlags, isTask } from '../use/use-task'; import { ComputedSignalImpl } from './impl/computed-signal-impl'; import { SignalImpl } from './impl/signal-impl'; import type { WrappedSignalImpl } from './impl/wrapped-signal-impl'; import type { Signal } from './signal.public'; -import { SubscriptionData, type NodePropPayload } from './subscription-data'; +import { SubscriptionData, type NodeProp } from './subscription-data'; import { SerializationSignalFlags, EffectProperty, @@ -27,6 +23,10 @@ import { type EffectSubscription, type StoreTarget, } from './types'; +import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; +import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; +import { setNodeDiffPayload, setNodePropData } from '../shared/cursor/chore-execution'; +import type { VNode } from '../shared/vnode/vnode'; const DEBUG = false; @@ -77,7 +77,7 @@ export const addQrlToSerializationCtx = ( } else if (effect instanceof ComputedSignalImpl) { qrl = effect.$computeQrl$; } else if (property === EffectProperty.COMPONENT) { - qrl = container.getHostProp(effect as ISsrNode, OnRenderProp); + qrl = container.getHostProp(effect as VNode, OnRenderProp); } if (qrl) { (container as SSRContainer).serializationCtx.$eventQrls$.add(qrl); @@ -98,45 +98,27 @@ export const scheduleEffects = ( assertDefined(container, 'Container must be defined.'); if (isTask(consumer)) { consumer.$flags$ |= TaskFlags.DIRTY; - DEBUG && log('schedule.consumer.task', pad('\n' + String(consumer), ' ')); - let choreType = ChoreType.TASK; - if (consumer.$flags$ & TaskFlags.VISIBLE_TASK) { - choreType = ChoreType.VISIBLE; - } - container.$scheduler$(choreType, consumer); + markVNodeDirty(container, consumer.$el$, ChoreBits.TASKS); } else if (consumer instanceof SignalImpl) { - // we don't schedule ComputedSignal/DerivedSignal directly, instead we invalidate it and - // and schedule the signals effects (recursively) - if (consumer instanceof ComputedSignalImpl) { - // Ensure that the computed signal's QRL is resolved. - // If not resolved schedule it to be resolved. - if (!consumer.$computeQrl$.resolved) { - container.$scheduler$(ChoreType.QRL_RESOLVE, null, consumer.$computeQrl$); - } - } - (consumer as ComputedSignalImpl | WrappedSignalImpl).invalidate(); } else if (property === EffectProperty.COMPONENT) { - const host: HostElement = consumer as any; - const qrl = container.getHostProp>>(host, OnRenderProp); - assertDefined(qrl, 'Component must have QRL'); - const props = container.getHostProp(host, ELEMENT_PROPS); - container.$scheduler$(ChoreType.COMPONENT, host, qrl, props); + markVNodeDirty(container, consumer, ChoreBits.COMPONENT); } else if (property === EffectProperty.VNODE) { if (isBrowser) { - const host: HostElement = consumer; - container.$scheduler$(ChoreType.NODE_DIFF, host, host, signal as SignalImpl); + setNodeDiffPayload(consumer, signal as Signal); + markVNodeDirty(container, consumer, ChoreBits.NODE_DIFF); } } else { - const host: HostElement = consumer; const effectData = effectSubscription[EffectSubscriptionProp.DATA]; if (effectData instanceof SubscriptionData) { const data = effectData.data; - const payload: NodePropPayload = { - ...data, - $value$: signal as SignalImpl, + const payload: NodeProp = { + isConst: data.$isConst$, + scopedStyleIdPrefix: data.$scopedStyleIdPrefix$, + value: signal as SignalImpl, }; - container.$scheduler$(ChoreType.NODE_PROP, host, property, payload); + setNodePropData(consumer, property, payload); + markVNodeDirty(container, consumer, ChoreBits.NODE_PROPS); } } }; diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts index 31613b7b426..17515bd2547 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -2,7 +2,7 @@ import { isDev } from '@qwik.dev/core/build'; import { vnode_isVNode } from '../client/vnode'; import { isSignal } from '../reactive-primitives/utils'; import { clearAllEffects } from '../reactive-primitives/cleanup'; -import { invokeApply, newInvokeContext, untrack } from '../use/use-core'; +import { invokeApply, newInvokeContext, untrack, type RenderInvokeContext } from '../use/use-core'; import { type EventQRL, type UseOnMap } from '../use/use-on'; import { isQwikComponent, type OnRenderFn } from './component.public'; import { assertDefined } from './error/assert'; @@ -63,7 +63,7 @@ export const executeComponent = ( subscriptionHost || undefined, undefined, RenderEvent - ); + ) as RenderInvokeContext; if (subscriptionHost) { iCtx.$effectSubscriber$ = getSubscriber(subscriptionHost, EffectProperty.COMPONENT); iCtx.$container$ = container; @@ -77,6 +77,7 @@ export const executeComponent = ( } if (isQrl(componentQRL)) { props = props || container.getHostProp(renderHost, ELEMENT_PROPS) || EMPTY_OBJ; + // TODO is this possible? JSXNode handles this, no? if ('children' in props) { delete props.children; } @@ -106,7 +107,7 @@ export const executeComponent = ( clearAllEffects(container, renderHost); } - return componentFn(props); + return maybeThen(componentFn(props), (jsx) => maybeThen(iCtx.$waitOn$, () => jsx)); }, (jsx) => { const useOnEvents = container.getHostProp(renderHost, USE_ON_LOCAL); diff --git a/packages/qwik/src/core/shared/cursor/chore-execution.ts b/packages/qwik/src/core/shared/cursor/chore-execution.ts new file mode 100644 index 00000000000..3424d60300f --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/chore-execution.ts @@ -0,0 +1,354 @@ +import { vnode_isVNode, type VNodeJournal } from '../../client/vnode'; +import { vnode_diff } from '../../client/vnode-diff'; +import { clearAllEffects } from '../../reactive-primitives/cleanup'; +import { runResource, type ResourceDescriptor } from '../../use/use-resource'; +import { Task, TaskFlags, runTask, type TaskFn } from '../../use/use-task'; +import { executeComponent } from '../component-execution'; +import { isServerPlatform } from '../platform/platform'; +import type { OnRenderFn } from '../component.public'; +import type { Props } from '../jsx/jsx-runtime'; +import type { QRLInternal } from '../qrl/qrl-class'; +import { ChoreBits } from '../vnode/enums/chore-bits.enum'; +import { + ELEMENT_SEQ, + ELEMENT_PROPS, + OnRenderProp, + QScopedStyle, + NODE_PROPS_DATA_KEY, + NODE_DIFF_DATA_KEY, + HOST_SIGNAL, +} from '../utils/markers'; +import { addComponentStylePrefix } from '../utils/scoped-styles'; +import { isPromise, maybeThen, retryOnPromise, safeCall } from '../utils/promises'; +import type { ValueOrPromise } from '../utils/types'; +import type { Container, HostElement } from '../types'; +import type { VNode } from '../vnode/vnode'; +import { VNodeFlags, type ClientContainer } from '../../client/types'; +import type { NodeProp } from '../../reactive-primitives/subscription-data'; +import { isSignal, scheduleEffects } from '../../reactive-primitives/utils'; +import type { Signal } from '../../reactive-primitives/signal.public'; +import { serializeAttribute } from '../utils/styles'; +import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types'; +import type { ElementVNode } from '../vnode/element-vnode'; +import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum'; +import type { JSXOutput } from '../jsx/types/jsx-node'; +import { type CursorData } from './cursor-props'; +import { invoke, newInvokeContext } from '../../use/use-core'; +import type { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl'; +import { SignalFlags } from '../../reactive-primitives/types'; + +/** + * Executes tasks for a vNode if the TASKS dirty bit is set. Tasks are stored in the ELEMENT_SEQ + * property and executed in order. + * + * Behavior: + * + * - Resources: Just run, don't save promise anywhere + * - Tasks: Chain promises only between each other + * - VisibleTasks: Store promises in afterFlush on cursor root for client, we need to wait for all + * visible tasks to complete before flushing changes to the DOM. On server, we keep them on vNode + * for streaming. + * + * @param vNode - The vNode to execute tasks for + * @param container - The container + * @param cursor - The cursor root vNode, should be set on client only + * @returns Promise if any regular task returns a promise, void otherwise + */ +export function executeTasks( + vNode: VNode, + container: Container, + cursorData: CursorData +): ValueOrPromise { + vNode.dirty &= ~ChoreBits.TASKS; + + const elementSeq = container.getHostProp(vNode, ELEMENT_SEQ); + + if (!elementSeq || elementSeq.length === 0) { + // No tasks to execute, clear the bit + return; + } + + // Execute all tasks in sequence + let taskPromise: Promise | undefined; + + for (const item of elementSeq) { + if (item instanceof Task) { + const task = item as Task; + + // Skip if task is not dirty + if (!(task.$flags$ & TaskFlags.DIRTY)) { + continue; + } + + // Check if it's a resource + if (task.$flags$ & TaskFlags.RESOURCE) { + // Resources: just run, don't save promise anywhere + runResource(task as ResourceDescriptor, container, vNode); + } else if (task.$flags$ & TaskFlags.VISIBLE_TASK) { + // VisibleTasks: store for execution after flush (don't execute now) + (cursorData.afterFlushTasks ||= []).push(task); + } else { + // Regular tasks: chain promises only between each other + const result = runTask(task, container, vNode); + if (isPromise(result)) { + if (task.$flags$ & TaskFlags.RENDER_BLOCKING) { + taskPromise = taskPromise + ? taskPromise.then(() => result as Promise) + : (result as Promise); + } else { + // TODO: set extrapromises on vNode instead of cursorData if server + const extraPromises = (cursorData.extraPromises ||= []); + extraPromises.push(result as Promise); + } + } + } + } + } + + return taskPromise; +} + +function getNodeDiffPayload(vNode: VNode): JSXOutput | null { + const props = vNode.props as Props; + return props[NODE_DIFF_DATA_KEY] as JSXOutput | null; +} + +export function setNodeDiffPayload(vNode: VNode, payload: JSXOutput | Signal): void { + const props = vNode.props as Props; + props[NODE_DIFF_DATA_KEY] = payload; +} + +export function executeNodeDiff( + vNode: VNode, + container: Container, + journal: VNodeJournal +): ValueOrPromise { + vNode.dirty &= ~ChoreBits.NODE_DIFF; + + const domVNode = vNode as ElementVNode; + let jsx = getNodeDiffPayload(vNode); + if (!jsx) { + return; + } + if (isSignal(jsx)) { + jsx = jsx.value as any; + } + const result = vnode_diff(container as ClientContainer, journal, jsx, domVNode, null); + return result; +} + +/** + * Executes a component for a vNode if the COMPONENT dirty bit is set. Gets the component QRL from + * OnRenderProp and executes it. + * + * @param vNode - The vNode to execute component for + * @param container - The container + * @returns Promise if component execution is async, void otherwise + */ +export function executeComponentChore( + vNode: VNode, + container: Container, + journal: VNodeJournal +): ValueOrPromise { + vNode.dirty &= ~ChoreBits.COMPONENT; + const host = vNode as HostElement; + const componentQRL = container.getHostProp> | null>( + host, + OnRenderProp + ); + + if (!componentQRL) { + return; + } + + const isServer = isServerPlatform(); + const props = container.getHostProp(host, ELEMENT_PROPS) || null; + + const result = safeCall( + () => executeComponent(container, host, host, componentQRL, props), + (jsx) => { + if (isServer) { + return jsx; + } else { + const styleScopedId = container.getHostProp(host, QScopedStyle); + return retryOnPromise(() => + vnode_diff( + container as ClientContainer, + journal, + jsx, + host, + addComponentStylePrefix(styleScopedId) + ) + ); + } + }, + (err: any) => { + container.handleError(err, host); + } + ); + + if (isPromise(result)) { + return result as Promise; + } + + return; +} + +/** + * Gets node prop data from a vNode. + * + * @param vNode - The vNode to get node prop data from + * @returns Array of NodeProp, or null if none + */ +function getNodePropData(vNode: VNode): Map | null { + const props = vNode.props as Props; + return (props[NODE_PROPS_DATA_KEY] as Map | null) ?? null; +} + +/** + * Sets node prop data for a vNode. + * + * @param vNode - The vNode to set node prop data for + * @param property - The property to set node prop data for + * @param nodeProp - The node prop data to set + */ +export function setNodePropData(vNode: VNode, property: string, nodeProp: NodeProp): void { + const props = vNode.props as Props; + let data = props[NODE_PROPS_DATA_KEY] as Map | null; + if (!data) { + data = new Map(); + props[NODE_PROPS_DATA_KEY] = data; + } + data.set(property, nodeProp); +} + +/** + * Clears node prop data from a vNode. + * + * @param vNode - The vNode to clear node prop data from + */ +function clearNodePropData(vNode: VNode): void { + const props = vNode.props as Props; + delete props[NODE_PROPS_DATA_KEY]; +} + +function setNodeProp( + domVNode: ElementVNode, + journal: VNodeJournal, + property: string, + value: string | boolean | null, + isConst: boolean +): void { + journal.push({ + operationType: VNodeOperationType.SetAttribute, + target: domVNode.node!, + attrName: property, + attrValue: value, + }); + if (!isConst) { + if (domVNode.props && value == null) { + delete domVNode.props[property]; + } else { + (domVNode.props ||= {})[property] = value; + } + } +} + +/** + * Executes node prop updates for a vNode if the NODE_PROPS dirty bit is set. Processes all pending + * node prop updates that were stored via addPendingNodeProp. + * + * @param vNode - The vNode to execute node props for + * @param container - The container + * @returns Void + */ +export function executeNodeProps(vNode: VNode, container: Container, journal: VNodeJournal): void { + vNode.dirty &= ~ChoreBits.NODE_PROPS; + if (!(vNode.flags & VNodeFlags.Element)) { + return; + } + + const allPropData = getNodePropData(vNode); + if (!allPropData || allPropData.size === 0) { + return; + } + + const domVNode = vNode as ElementVNode; + + const isServer = isServerPlatform(); + + // Process all pending node prop updates + for (const [property, nodeProp] of allPropData.entries()) { + let value: Signal | string = nodeProp.value; + if (isSignal(value)) { + // TODO: Handle async signals (promises) - need to track pending async prop data + value = value.value as any; + } + + // Process synchronously (same logic as scheduler) + const serializedValue = serializeAttribute(property, value, nodeProp.scopedStyleIdPrefix); + if (isServer) { + (container as SSRContainer).addBackpatchEntry( + // TODO: type + (vNode as unknown as ISsrNode).id, + property, + serializedValue + ); + } else { + const isConst = nodeProp.isConst; + setNodeProp(domVNode, journal, property, serializedValue, isConst); + } + } + + // Clear pending prop data after processing + clearNodePropData(vNode); +} + +/** + * Execute visible task cleanups and add promises to extraPromises. + * + * @param vNode - The vNode to cleanup + * @param container - The container + * @returns Void + */ +export function executeCleanup(vNode: VNode, container: Container): void { + vNode.dirty &= ~ChoreBits.CLEANUP; + + if (vnode_isVNode(vNode)) { + // TODO I dont think this runs the cleanups of visible tasks + // TODO add promises to extraPromises + clearAllEffects(container, vNode); + } +} + +/** + * Executes compute/recompute chores for a vNode if the COMPUTE dirty bit is set. This handles + * signal recomputation and effect scheduling. + * + * @param vNode - The vNode to execute compute for + * @param container - The container + * @returns Promise if computation is async, void otherwise + */ +export function executeCompute(vNode: VNode, container: Container): ValueOrPromise { + vNode.dirty &= ~ChoreBits.COMPUTE; + const target = container.getHostProp | null>(vNode, HOST_SIGNAL); + if (!target) { + return; + } + const effects = target.$effects$; + + const ctx = newInvokeContext(); + ctx.$container$ = container; + // needed for computed signals and throwing QRLs + return maybeThen( + retryOnPromise(() => + invoke.call(target, ctx, (target as WrappedSignalImpl).$computeIfNeeded$) + ), + () => { + if ((target as WrappedSignalImpl).$flags$ & SignalFlags.RUN_EFFECTS) { + (target as WrappedSignalImpl).$flags$ &= ~SignalFlags.RUN_EFFECTS; + return scheduleEffects(container, target, effects); + } + } + ); +} diff --git a/packages/qwik/src/core/shared/cursor/cursor-flush.ts b/packages/qwik/src/core/shared/cursor/cursor-flush.ts new file mode 100644 index 00000000000..978bec1ebbc --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-flush.ts @@ -0,0 +1,121 @@ +import type { VNodeJournal } from '../../client/vnode'; +import { runTask } from '../../use/use-task'; +import { QContainerValue, type Container } from '../types'; +import { dangerouslySetInnerHTML, QContainerAttr } from '../utils/markers'; +import { VNodeOperationType } from '../vnode/enums/vnode-operation-type.enum'; +import type { Cursor } from './cursor'; +import { getCursorData, type CursorData } from './cursor-props'; + +/** + * Executes the flush phase for a cursor. + * + * @param cursor - The cursor to execute the flush phase for + * @param container - The container to execute the flush phase for + */ +export function executeFlushPhase(cursor: Cursor, container: Container): void { + const cursorData = getCursorData(cursor)!; + const journal = cursorData.journal; + if (journal && journal.length > 0) { + _flushJournal(journal); + cursorData.journal = null; + } + executeAfterFlush(container, cursorData); +} + +export function _flushJournal(journal: VNodeJournal): void { + for (const operation of journal) { + switch (operation.operationType) { + case VNodeOperationType.InsertOrMove: { + const insertBefore = operation.beforeTarget; + const insertBeforeParent = operation.parent; + insertBeforeParent.insertBefore(operation.target, insertBefore); + break; + } + case VNodeOperationType.Delete: { + operation.target.remove(); + break; + } + case VNodeOperationType.SetText: { + operation.target.nodeValue = operation.text; + break; + } + case VNodeOperationType.SetAttribute: { + const element = operation.target; + const attrName = operation.attrName; + const attrValue = operation.attrValue; + const shouldRemove = attrValue == null || attrValue === false; + if (isBooleanAttr(element, attrName)) { + (element as any)[attrName] = parseBoolean(attrValue); + } else if (attrName === dangerouslySetInnerHTML) { + (element as any).innerHTML = attrValue; + element.setAttribute(QContainerAttr, QContainerValue.HTML); + } else if (shouldRemove) { + element.removeAttribute(attrName); + } else if (attrName === 'value' && attrName in element) { + (element as any).value = attrValue; + } else { + element.setAttribute(attrName, attrValue as string); + } + break; + } + case VNodeOperationType.RemoveAllChildren: { + const removeParent = operation.target; + if (removeParent.replaceChildren) { + removeParent.replaceChildren(); + } else { + // fallback if replaceChildren is not supported + removeParent.textContent = ''; + } + break; + } + } + } +} + +function executeAfterFlush(container: Container, cursorData: CursorData): void { + const visibleTasks = cursorData.afterFlushTasks; + if (!visibleTasks || visibleTasks.length === 0) { + return; + } + for (const visibleTask of visibleTasks) { + const task = visibleTask; + runTask(task, container, task.$el$); + } + cursorData.afterFlushTasks = null; +} + +const isBooleanAttr = (element: Element, key: string): boolean => { + const isBoolean = + key == 'allowfullscreen' || + key == 'async' || + key == 'autofocus' || + key == 'autoplay' || + key == 'checked' || + key == 'controls' || + key == 'default' || + key == 'defer' || + key == 'disabled' || + key == 'formnovalidate' || + key == 'inert' || + key == 'ismap' || + key == 'itemscope' || + key == 'loop' || + key == 'multiple' || + key == 'muted' || + key == 'nomodule' || + key == 'novalidate' || + key == 'open' || + key == 'playsinline' || + key == 'readonly' || + key == 'required' || + key == 'reversed' || + key == 'selected'; + return isBoolean && key in element; +}; + +const parseBoolean = (value: string | boolean | null): boolean => { + if (value === 'false') { + return false; + } + return Boolean(value); +}; diff --git a/packages/qwik/src/core/shared/cursor/cursor-props.ts b/packages/qwik/src/core/shared/cursor/cursor-props.ts new file mode 100644 index 00000000000..33436f23d1e --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-props.ts @@ -0,0 +1,99 @@ +import type { VNode } from '../vnode/vnode'; +import { isCursor } from './cursor'; +import { removeCursorFromQueue } from './cursor-queue'; +import type { Container } from '../types'; +import type { VNodeJournal } from '../../client/vnode'; +import type { Task } from '../../use/use-task'; +import { resolveCursor } from './cursor-walker'; + +/** + * Keys used to store cursor-related data in vNode props. These are internal properties that should + * not conflict with user props. + */ +const CURSOR_DATA_KEY = ':cursor'; + +export interface CursorData { + afterFlushTasks: Task[] | null; + extraPromises: Promise[] | null; + journal: VNodeJournal | null; + container: Container; + position: VNode | null; + priority: number; + promise: Promise | null; +} + +/** + * Sets the cursor position in a cursor vNode. + * + * @param vNode - The cursor vNode + * @param position - The vNode position to set, or null for root + */ +export function setCursorPosition( + container: Container, + cursorData: CursorData, + position: VNode | null +): void { + cursorData.position = position; + if (position && isCursor(position)) { + mergeCursors(container, cursorData, position); + } +} + +function mergeCursors(container: Container, newCursorData: CursorData, oldCursor: VNode): void { + // delete from global cursors queue + removeCursorFromQueue(oldCursor); + resolveCursor(container); + const oldCursorData = getCursorData(oldCursor)!; + // merge after flush tasks + const oldAfterFlushTasks = oldCursorData.afterFlushTasks; + if (oldAfterFlushTasks && oldAfterFlushTasks.length > 0) { + const newAfterFlushTasks = newCursorData.afterFlushTasks; + if (newAfterFlushTasks) { + newAfterFlushTasks.push(...oldAfterFlushTasks); + } else { + newCursorData.afterFlushTasks = oldAfterFlushTasks; + } + } + // merge extra promises + const oldExtraPromises = oldCursorData.extraPromises; + if (oldExtraPromises && oldExtraPromises.length > 0) { + const newExtraPromises = newCursorData.extraPromises; + if (newExtraPromises) { + newExtraPromises.push(...oldExtraPromises); + } else { + newCursorData.extraPromises = oldExtraPromises; + } + } + // merge journal + const oldJournal = oldCursorData.journal; + if (oldJournal && oldJournal.length > 0) { + const newJournal = newCursorData.journal; + if (newJournal) { + newJournal.push(...oldJournal); + } else { + newCursorData.journal = oldJournal; + } + } +} + +/** + * Gets the cursor data from a vNode. + * + * @param vNode - The vNode + * @returns The cursor data, or null if none or not a cursor + */ +export function getCursorData(vNode: VNode): CursorData | null { + const props = vNode.props; + return (props?.[CURSOR_DATA_KEY] as CursorData | null) ?? null; +} + +/** + * Sets the cursor data on a vNode. + * + * @param vNode - The vNode + * @param cursorData - The cursor data to set, or null to clear + */ +export function setCursorData(vNode: VNode, cursorData: CursorData | null): void { + const props = (vNode.props ||= {}); + props[CURSOR_DATA_KEY] = cursorData; +} diff --git a/packages/qwik/src/core/shared/cursor/cursor-queue.ts b/packages/qwik/src/core/shared/cursor/cursor-queue.ts new file mode 100644 index 00000000000..b7a77f2c81d --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-queue.ts @@ -0,0 +1,89 @@ +/** + * @file Cursor queue management for cursor-based scheduling. + * + * Maintains a priority queue of cursors sorted by priority (lower = higher priority). + */ + +import { VNodeFlags } from '../../client/types'; +import type { Container } from '../types'; +import type { Cursor } from './cursor'; +import { getCursorData } from './cursor-props'; + +/** Global cursor queue array. Cursors are sorted by priority. */ +let globalCursorQueue: Cursor[] = []; + +/** + * Adds a cursor to the global queue. If the cursor already exists, it's removed and re-added to + * maintain correct priority order. + * + * @param cursor - The cursor to add + */ +export function addCursorToQueue(container: Container, cursor: Cursor): void { + const priority = getCursorData(cursor)!.priority; + let insertIndex = globalCursorQueue.length; + + for (let i = 0; i < globalCursorQueue.length; i++) { + const existingPriority = getCursorData(globalCursorQueue[i])!.priority; + if (priority < existingPriority) { + insertIndex = i; + break; + } + } + + globalCursorQueue.splice(insertIndex, 0, cursor); + + container.$cursorCount$++; + container.$renderPromise$ ||= new Promise((r) => (container.$resolveRenderPromise$ = r)); +} + +/** + * Gets the highest priority cursor (lowest priority number) from the queue. + * + * @returns The highest priority cursor, or null if queue is empty + */ +export function getHighestPriorityCursor(): Cursor | null { + return globalCursorQueue.length > 0 ? globalCursorQueue[0] : null; +} + +/** + * Removes a cursor from the global queue. + * + * @param cursor - The cursor to remove + */ +export function removeCursorFromQueue(cursor: Cursor): void { + cursor.flags &= ~VNodeFlags.Cursor; + const index = globalCursorQueue.indexOf(cursor); + if (index !== -1) { + // TODO: we can't use swap-and-remove algorithm because it will break the priority order + // // Move last element to the position of the element to remove, then pop + // const lastIndex = globalCursorQueue.length - 1; + // if (index !== lastIndex) { + // globalCursorQueue[index] = globalCursorQueue[lastIndex]; + // } + // globalCursorQueue.pop(); + globalCursorQueue.splice(index, 1); + } +} + +/** + * Checks if the global cursor queue is empty. + * + * @returns True if the queue is empty + */ +export function isCursorQueueEmpty(): boolean { + return globalCursorQueue.length === 0; +} + +/** + * Gets the number of cursors in the global queue. + * + * @returns The number of cursors + */ +export function getCursorQueueSize(): number { + return globalCursorQueue.length; +} + +/** Clears all cursors from the global queue. */ +export function clearCursorQueue(): void { + globalCursorQueue = []; +} diff --git a/packages/qwik/src/core/shared/cursor/cursor-walker.ts b/packages/qwik/src/core/shared/cursor/cursor-walker.ts new file mode 100644 index 00000000000..56150dc312c --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor-walker.ts @@ -0,0 +1,243 @@ +/** + * @file Cursor walker implementation for cursor-based scheduling. + * + * Implements depth-first traversal of the vDOM tree, processing dirty vNodes and their children. + * Handles promise blocking, time-slicing, and cursor position tracking. + */ + +import { isServerPlatform } from '../platform/platform'; +import type { VNode } from '../vnode/vnode'; +import { + executeCleanup, + executeComponentChore, + executeCompute, + executeNodeDiff, + executeNodeProps, + executeTasks, +} from './chore-execution'; +import { type Cursor } from './cursor'; +import { setCursorPosition, getCursorData } from './cursor-props'; +import { ChoreBits } from '../vnode/enums/chore-bits.enum'; +import { addCursorToQueue, getHighestPriorityCursor, removeCursorFromQueue } from './cursor-queue'; +import { executeFlushPhase } from './cursor-flush'; +import { createNextTick } from '../platform/next-tick'; +import { isPromise } from '../utils/promises'; +import type { ValueOrPromise } from '../utils/types'; +import { assertDefined } from '../error/assert'; +import type { Container } from '../types'; +import { VNodeFlags } from '../../client/types'; + +const DEBUG = false; + +const nextTick = createNextTick(processCursorQueue); +let isNextTickScheduled = false; + +export function triggerCursors(): void { + if (!isNextTickScheduled) { + isNextTickScheduled = true; + nextTick(); + } +} + +/** Options for walking a cursor. */ +export interface WalkOptions { + /** Time budget in milliseconds (for DOM time-slicing). If exceeded, walk pauses. */ + timeBudget: number; +} + +/** + * Processes the cursor queue, walking each cursor in turn. + * + * @param options - Walk options (time budget, etc.) + */ +export function processCursorQueue( + options: WalkOptions = { + timeBudget: 1000 / 60, // 60fps + } +): void { + isNextTickScheduled = false; + + let cursor: Cursor | null = null; + while ((cursor = getHighestPriorityCursor())) { + walkCursor(cursor, options); + } +} + +/** + * Walks a cursor through the vDOM tree, processing dirty vNodes in depth-first order. + * + * The walker: + * + * 1. Starts from the cursor root (or resumes from cursor position) + * 2. Processes dirty vNodes using executeChoreSequence + * 3. If the vNode is not dirty, moves to the next vNode + * 4. If the vNode is dirty, executes the chores + * 5. If the chore is a promise, pauses the cursor and resumes in next tick + * 6. If the time budget is exceeded, pauses the cursor and resumes in next tick + * 7. Updates cursor position as it walks + * + * Note that there is only one walker for all containers in the app with the same Qwik version. + * + * @param cursor - The cursor to walk + * @param options - Walk options (time budget, etc.) + * @returns Walk result indicating completion status + */ +export function walkCursor(cursor: Cursor, options: WalkOptions): void { + const { timeBudget } = options; + const isServer = isServerPlatform(); + const startTime = performance.now(); + + const cursorData = getCursorData(cursor)!; + + // Check if cursor is blocked by a promise + const blockingPromise = cursorData.promise; + if (blockingPromise) { + return; + } + + const container = cursorData.container; + assertDefined(container, 'Cursor container not found'); + + // Check if cursor is already complete + if (!cursor.dirty) { + finishWalk(container, cursor, isServer); + return; + } + + const journal = (cursorData.journal ||= []); + + // Get starting position (resume from last position or start at root) + let currentVNode: VNode | null = null; + + let count = 0; + while ((currentVNode = cursorData.position)) { + DEBUG && console.warn('walkCursor', currentVNode.toString()); + if (count++ > 100) { + throw new Error('Infinite loop detected in cursor walker'); + } + // Check time budget (only for DOM, not SSR) + if (!isServer && !import.meta.env.TEST) { + const elapsed = performance.now() - startTime; + if (elapsed >= timeBudget) { + // Run in next tick + triggerCursors(); + return; + } + } + + // Skip if the vNode is not dirty + if (!(currentVNode.dirty & ChoreBits.DIRTY_MASK) || cursorData.promise) { + // Move to next node + setCursorPosition(container, cursorData, getNextVNode(currentVNode)); + continue; + } + + // Skip if the vNode is deleted + if (currentVNode.flags & VNodeFlags.Deleted) { + // Clear dirty bits and move to next node + currentVNode.dirty &= ~ChoreBits.DIRTY_MASK; + setCursorPosition(container, cursorData, getNextVNode(currentVNode)); + continue; + } + + let result: ValueOrPromise | undefined; + try { + // Execute chores in order + if (currentVNode.dirty & ChoreBits.TASKS) { + result = executeTasks(currentVNode, container, cursorData); + } else if (currentVNode.dirty & ChoreBits.NODE_DIFF) { + result = executeNodeDiff(currentVNode, container, journal); + } else if (currentVNode.dirty & ChoreBits.COMPONENT) { + result = executeComponentChore(currentVNode, container, journal); + } else if (currentVNode.dirty & ChoreBits.NODE_PROPS) { + executeNodeProps(currentVNode, container, journal); + } else if (currentVNode.dirty & ChoreBits.COMPUTE) { + result = executeCompute(currentVNode, container); + } else if (currentVNode.dirty & ChoreBits.CHILDREN) { + const dirtyChildren = currentVNode.dirtyChildren; + if (!dirtyChildren || dirtyChildren.length === 0) { + // No dirty children + currentVNode.dirty &= ~ChoreBits.CHILDREN; + } else { + currentVNode.nextDirtyChildIndex = 0; + // descend + currentVNode = getNextVNode(dirtyChildren[0])!; + setCursorPosition(container, cursorData, currentVNode); + continue; + } + } else if (currentVNode.dirty & ChoreBits.CLEANUP) { + executeCleanup(currentVNode, container); + } + } catch (error) { + container.handleError(error, currentVNode); + } + + // Handle blocking promise + if (result && isPromise(result)) { + DEBUG && console.warn('walkCursor: blocking promise', currentVNode.toString()); + // Store promise on cursor and pause + cursorData.promise = result; + removeCursorFromQueue(cursor); + + const host = currentVNode; + result + .catch((error) => { + cursorData.promise = null; + container.handleError(error, host); + }) + .finally(() => { + cursorData.promise = null; + addCursorToQueue(container, cursor); + triggerCursors(); + }); + } + } + finishWalk(container, cursor, isServer); +} + +function finishWalk(container: Container, cursor: Cursor, isServer: boolean): void { + if (!(cursor.dirty & ChoreBits.DIRTY_MASK)) { + removeCursorFromQueue(cursor); + if (!isServer) { + executeFlushPhase(cursor, container); + } + resolveCursor(container); + } +} + +export function resolveCursor(container: Container): void { + // TODO streaming as a cursor? otherwise we need to wait separately for it + // or just ignore and resolve manually + if (--container.$cursorCount$ === 0) { + container.$resolveRenderPromise$!(); + container.$renderPromise$ = null; + } +} + +/** @returns Next vNode to process, or null if traversal is complete */ +function getNextVNode(vNode: VNode): VNode | null { + const parent = vNode.parent || vNode.slotParent; + if (!parent || !(parent.dirty & ChoreBits.CHILDREN)) { + return null; + } + const dirtyChildren = parent.dirtyChildren!; + let index = parent.nextDirtyChildIndex; + + const len = dirtyChildren!.length; + let count = len; + while (count-- > 0) { + const nextVNode = dirtyChildren[index]; + if (nextVNode.dirty & ChoreBits.DIRTY_MASK) { + parent.nextDirtyChildIndex = (index + 1) % len; + return nextVNode; + } + index++; + if (index === len) { + index = 0; + } + } + // all array items checked, children are no longer dirty + parent!.dirty &= ~ChoreBits.CHILDREN; + parent!.dirtyChildren = null; + return getNextVNode(parent!); +} diff --git a/packages/qwik/src/core/shared/cursor/cursor.ts b/packages/qwik/src/core/shared/cursor/cursor.ts new file mode 100644 index 00000000000..108740ac780 --- /dev/null +++ b/packages/qwik/src/core/shared/cursor/cursor.ts @@ -0,0 +1,82 @@ +import { VNodeFlags } from '../../client/types'; +import type { Container } from '../types'; +import type { VNode } from '../vnode/vnode'; +import { type CursorData, setCursorData } from './cursor-props'; +import { addCursorToQueue } from './cursor-queue'; +import { triggerCursors } from './cursor-walker'; + +/** + * A cursor is a vNode that has the CURSOR flag set and priority stored in props. + * + * The cursor root is the vNode where the cursor was created (the dirty root). The cursor's current + * position is tracked in the vNode's props. + */ +export type Cursor = VNode; + +/** + * Adds a cursor to the given vNode (makes the vNode a cursor). Sets the cursor priority and + * position to the root vNode itself. + * + * @param root - The vNode that will become the cursor root (dirty root) + * @param priority - Priority level (lower = higher priority, 0 is default) + * @returns The vNode itself, now acting as a cursor + */ +export function addCursor(container: Container, root: VNode, priority: number): Cursor { + const cursorData: CursorData = { + afterFlushTasks: null, + extraPromises: null, + journal: null, + container: container, + position: root, + priority: priority, + promise: null, + }; + + setCursorData(root, cursorData); + + const cursor = root as Cursor; + cursor.flags |= VNodeFlags.Cursor; + // Add cursor to global queue + addCursorToQueue(container, cursor); + + triggerCursors(); + + return cursor; +} + +/** + * Checks if a vNode is a cursor (has CURSOR flag set). + * + * @param vNode - The vNode to check + * @returns True if the vNode has the CURSOR flag set + */ +export function isCursor(vNode: VNode): vNode is Cursor { + return (vNode.flags & VNodeFlags.Cursor) !== 0; +} + +/** + * Checks if a cursor is complete (root vNode is clean). According to RFC section 3.2: "when a + * cursor finally marks its root vNode clean, that means the entire subtree is clean." + * + * @param cursor - The cursor to check + * @returns True if the cursor's root vNode has no dirty bits + */ +export function isCursorComplete(cursor: Cursor): boolean { + return cursor.dirty === 0; +} + +/** + * Finds the root cursor for the given vNode. + * + * @param vNode - The vNode to find the cursor for + * @returns The cursor that contains the vNode, or null if no cursor is found + */ +export function findCursor(vNode: VNode): Cursor | null { + while (vNode) { + if (isCursor(vNode)) { + return vNode; + } + vNode = (vNode as VNode).parent || (vNode as VNode).slotParent!; + } + return null; +} diff --git a/packages/qwik/src/core/shared/jsx/jsx-node.ts b/packages/qwik/src/core/shared/jsx/jsx-node.ts index 20652bf3fce..d96a5b506af 100644 --- a/packages/qwik/src/core/shared/jsx/jsx-node.ts +++ b/packages/qwik/src/core/shared/jsx/jsx-node.ts @@ -82,7 +82,6 @@ export class JSXNodeImpl implements JSXNodeInternal { } } - // TODO let the optimizer do this instead if ('className' in this.varProps) { this.varProps.class = this.varProps.className; this.varProps.className = undefined; @@ -93,6 +92,7 @@ export class JSXNodeImpl implements JSXNodeInternal { ); } } + // TODO let the optimizer do this instead if (this.constProps && 'className' in this.constProps) { this.constProps.class = this.constProps.className; this.constProps.className = undefined; diff --git a/packages/qwik/src/core/shared/jsx/props-proxy.ts b/packages/qwik/src/core/shared/jsx/props-proxy.ts index 1b4e9f16948..fc7b4c09e40 100644 --- a/packages/qwik/src/core/shared/jsx/props-proxy.ts +++ b/packages/qwik/src/core/shared/jsx/props-proxy.ts @@ -10,7 +10,7 @@ import type { Props } from './jsx-runtime'; import type { JSXNodeInternal } from './types/jsx-node'; import type { Container } from '../types'; import { assertTrue } from '../error/assert'; -import { ChoreType } from '../util-chore-type'; +import { scheduleEffects } from '../../reactive-primitives/utils'; export function createPropsProxy(owner: JSXNodeImpl): Props { // TODO don't make a proxy but populate getters? benchmark @@ -174,12 +174,7 @@ const addPropsProxyEffect = (propsProxy: PropsProxyHandler, prop: string | symbo export const triggerPropsProxyEffect = (propsProxy: PropsProxyHandler, prop: string | symbol) => { const effects = getEffects(propsProxy.$effects$, prop); if (effects) { - propsProxy.$container$?.$scheduler$( - ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, - undefined, - propsProxy, - effects - ); + scheduleEffects(propsProxy.$container$, propsProxy, effects); } }; diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx b/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx new file mode 100644 index 00000000000..aa8dab5fcdb --- /dev/null +++ b/packages/qwik/src/core/shared/jsx/types/jsx-polymorphic.unit.tsx @@ -0,0 +1,70 @@ +import type { EventHandler, FunctionComponent, PropsOf } from '@qwik.dev/core'; +import { component$ } from '@qwik.dev/core'; +import { describe, expectTypeOf, test } from 'vitest'; + +// This is in a separate file because it makes TS very slow +describe('polymorphism', () => { + test('polymorphic component', () => () => { + const Poly = component$( + ({ + as, + ...props + }: { as?: C } & PropsOf) => { + const Cmp = as || 'div'; + return hi; + } + ); + expectTypeOf>[0]['popovertarget']>().toEqualTypeOf< + string | undefined + >(); + expectTypeOf>[0]['href']>().toEqualTypeOf(); + expectTypeOf>[0]>().not.toHaveProperty('href'); + expectTypeOf>[0]>().not.toHaveProperty('popovertarget'); + expectTypeOf< + Parameters[0]['onClick$'], EventHandler>>[1] + >().toEqualTypeOf(); + + const MyCmp = component$((p: { name: string }) => Hi {p.name}); + + return ( + <> + { + expectTypeOf(ev).not.toBeAny(); + expectTypeOf(ev).toEqualTypeOf(); + expectTypeOf(el).toEqualTypeOf(); + }} + // This should error + // popovertarget + > + Foo + + { + expectTypeOf(ev).not.toBeAny(); + expectTypeOf(ev).toEqualTypeOf(); + expectTypeOf(el).toEqualTypeOf(); + }} + href="hi" + // This should error + // popovertarget + > + Foo + + { + expectTypeOf(ev).not.toBeAny(); + expectTypeOf(ev).toEqualTypeOf(); + expectTypeOf(el).toEqualTypeOf(); + }} + popovertarget="foo" + > + Bar + + + + ); + }); +}); diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx b/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx index 5cc888c20b4..386c7c16fbc 100644 --- a/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx +++ b/packages/qwik/src/core/shared/jsx/types/jsx-types.unit.tsx @@ -148,6 +148,18 @@ describe('types', () => { type: 'button'; popovertarget?: string; }>().toMatchTypeOf>(); + + $((_, element) => { + element.select(); + expectTypeOf(element).toEqualTypeOf(); + }) as QRLEventHandlerMulti; + + const t = $>((_, element) => { + element.select(); + expectTypeOf(element).toEqualTypeOf(); + }); + expectTypeOf(t).toExtend>(); + <>