From 21813252e44683318d1976784bca1be22278525c Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 24 Nov 2024 11:59:58 +0100 Subject: [PATCH] Add internals for mount path --- src/diff/children.js | 5 ++- src/diff/mount.js | 80 ++++++++++++++++++++++++++++++-------------- src/internal.d.ts | 37 ++++++++++++++++++++ src/render.js | 6 ++-- src/tree.js | 70 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 src/tree.js diff --git a/src/diff/children.js b/src/diff/children.js index c296521ee5..dcc3cbd2e2 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -11,6 +11,7 @@ import { isArray } from '../util'; import { getDomSibling } from '../component'; import { mount } from './mount'; import { insert } from './operations'; +import { createInternal } from '../tree'; /** * Diff the children of a virtual node @@ -103,9 +104,11 @@ export function diffChildren( refQueue ); } else { + // TODO: temp + const internal = createInternal(childVNode, null); result = mount( parentDom, - childVNode, + internal, globalContext, namespace, excessDomChildren, diff --git a/src/diff/mount.js b/src/diff/mount.js index 7bf501172b..899a3b5797 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -11,11 +11,22 @@ import { insert } from './operations'; import { setProperty } from './props'; import { assign, isArray, slice } from '../util'; import options from '../options'; +import { + createInternal, + MODE_MATH, + MODE_SVG, + TYPE_CLASS, + TYPE_COMPONENT, + TYPE_ELEMENT, + TYPE_FUNCTION, + TYPE_INVALID, + TYPE_TEXT +} from '../tree'; /** * Diff two virtual nodes and apply proper changes to the DOM * @param {PreactElement} parentDom The parent of the DOM element - * @param {VNode} newVNode The new virtual node + * @param {Internal} internal The backing node. * @param {object} globalContext The current context object. Modified by * getChildContext * @param {string} namespace Current namespace of the DOM node (HTML, SVG, or MathML) @@ -31,7 +42,7 @@ import options from '../options'; */ export function mount( parentDom, - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -40,22 +51,24 @@ export function mount( isHydrating, refQueue ) { + // @ts-expect-error + const newVNode = internal.vnode; + // When passing through createElement it assigns the object // constructor as undefined. This to prevent JSON-injection. - if (newVNode.constructor !== UNDEFINED) return null; + if (internal.flags & TYPE_INVALID) return null; /** @type {any} */ - let tmp, - newType = newVNode.type; + let tmp; if ((tmp = options._diff)) tmp(newVNode); - if (typeof newType == 'function') { + if (internal.flags & TYPE_COMPONENT) { try { let c, - newProps = newVNode.props; - const isClassComponent = - 'prototype' in newType && newType.prototype.render; + newProps = internal.props, + newType = /** @type {ComponentType} */ (internal.type); + const isClassComponent = !!(internal.flags & TYPE_CLASS); // Necessary for createContext api. Setting this property will pass // the context value as `this.context` just for this component. @@ -69,11 +82,17 @@ export function mount( // Instantiate the new component if (isClassComponent) { - // @ts-expect-error The check above verifies that newType is suppose to be constructed - newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap + internal._component = + newVNode._component = + c = + // @ts-expect-error The check above verifies that newType is suppose to be constructed + new newType(newProps, componentContext); // eslint-disable-line new-cap } else { - // @ts-expect-error Trust me, Component implements the interface we want - newVNode._component = c = new BaseComponent(newProps, componentContext); + // @ts-expect-error The check above verifies that newType is suppose to be constructed + internal._component = + newVNode._component = + c = + new BaseComponent(newProps, componentContext); c.constructor = newType; c.render = doRender; } @@ -156,6 +175,7 @@ export function mount( let renderResult = isTopLevelFragment ? tmp.props.children : tmp; oldDom = mountChildren( + internal, parentDom, isArray(renderResult) ? renderResult : [renderResult], newVNode, @@ -200,7 +220,7 @@ export function mount( } } else { oldDom = newVNode._dom = mountElementNode( - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -217,7 +237,7 @@ export function mount( /** * Diff two virtual nodes representing DOM element - * @param {VNode} newVNode The new virtual node + * @param {Internal} internal The new virtual node * @param {object} globalContext The current context object * @param {string} namespace Current namespace of the DOM node (HTML, SVG, or MathML) * @param {Array} excessDomChildren @@ -228,7 +248,7 @@ export function mount( * @returns {PreactElement} */ function mountElementNode( - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -236,11 +256,13 @@ function mountElementNode( isHydrating, refQueue ) { + // @ts-expect-error + const newVNode = internal.vnode; /** @type {PreactElement} */ let dom; let oldProps = EMPTY_OBJ; - let newProps = newVNode.props; - let nodeType = /** @type {string} */ (newVNode.type); + let newProps = internal.props; + let nodeType = /** @type {string} */ (internal.type); /** @type {any} */ let i; /** @type {{ __html?: string }} */ @@ -252,8 +274,8 @@ function mountElementNode( let checked; // Tracks entering and exiting namespaces when descending through the tree. - if (nodeType === 'svg') namespace = 'http://www.w3.org/2000/svg'; - else if (nodeType === 'math') + if (internal.flags & MODE_SVG) namespace = 'http://www.w3.org/2000/svg'; + else if (internal.flags & MODE_MATH) namespace = 'http://www.w3.org/1998/Math/MathML'; else if (!namespace) namespace = 'http://www.w3.org/1999/xhtml'; @@ -277,7 +299,7 @@ function mountElementNode( } if (dom == null) { - if (nodeType === null) { + if (internal.flags & TYPE_TEXT) { return document.createTextNode(newProps); } @@ -298,7 +320,7 @@ function mountElementNode( excessDomChildren = null; } - if (nodeType === null) { + if (internal.flags & TYPE_TEXT) { // During hydration, we still have to split merged text from SSR'd HTML. dom.data = newProps; } else { @@ -361,6 +383,7 @@ function mountElementNode( newVNode._children = []; } else { mountChildren( + internal, dom, isArray(newChildren) ? newChildren : [newChildren], newVNode, @@ -416,6 +439,7 @@ function doRender(props, _state, context) { /** * Diff the children of a virtual node + * @param {Internal} internal The DOM element whose children are being * @param {PreactElement} parentDom The DOM element whose children are being * diffed * @param {ComponentChildren[]} renderResult @@ -435,6 +459,7 @@ function doRender(props, _state, context) { * @param {any[]} refQueue an array of elements needed to invoke refs */ function mountChildren( + internal, parentDom, renderResult, newParentVNode, @@ -517,10 +542,11 @@ function mountChildren( childVNode._parent = newParentVNode; childVNode._depth = newParentVNode._depth + 1; + const childInternal = createInternal(childVNode, internal); // Morph the old element into the new one, but don't append it to the dom yet const result = mount( parentDom, - childVNode, + childInternal, globalContext, namespace, excessDomChildren, @@ -544,9 +570,13 @@ function mountChildren( firstChildDom = newDom; } - if (typeof childVNode.type != 'function') { + if (childInternal.flags & TYPE_ELEMENT || childInternal.flags & TYPE_TEXT) { oldDom = insert(childVNode, oldDom, parentDom); - } else if (typeof childVNode.type == 'function' && result !== UNDEFINED) { + } else if ( + (childInternal.flags & TYPE_FUNCTION || + childInternal.flags & TYPE_CLASS) && + result !== UNDEFINED + ) { oldDom = result; } else if (newDom) { oldDom = newDom.nextSibling; diff --git a/src/internal.d.ts b/src/internal.d.ts index f5bff255da..569fb93c2e 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -159,6 +159,43 @@ declare global { _flags: number; } + /** + * An Internal is a persistent backing node within Preact's virtual DOM tree. + * Think of an Internal like a long-lived VNode with stored data and tree linkages. + */ + export interface Internal

{ + type: string | ComponentType

; + /** The props object for Elements/Components, and the string contents for Text */ + props: (P & { children: ComponentChildren }) | string | number; + key: any; + ref: Ref | null; + + /** Bitfield containing information about the Internal or its component. */ + flags: number; + /** Polymorphic property to store extensions like hooks on */ + data: object | PreactNode; + /** The function that triggers in-place re-renders for an internal */ + // rerender: (internal: Internal) => void; + + /** children Internal nodes */ + _children: Internal[]; + /** next sibling Internal node */ + _parent: Internal; + /** most recent vnode ID */ + _vnodeId: number; + /** + * Associated DOM element for the Internal, or its nearest realized descendant. + * For Fragments, this is the first DOM child. + */ + /** The component instance for which this is a backing Internal node */ + _component: Component | null; + /** This Internal's distance from the tree root */ + _depth: number | null; + /** Callbacks to invoke when this internal commits */ + _commitCallbacks: Array<() => void>; + _stateCallbacks: Array<() => void>; // Only class components + } + export interface Component

extends preact.Component { // When component is functional component, this is reset to functional component constructor: ComponentType

; diff --git a/src/render.js b/src/render.js index 951eaf4a25..7995f038b8 100644 --- a/src/render.js +++ b/src/render.js @@ -4,6 +4,7 @@ import { createElement, Fragment } from './create-element'; import options from './options'; import { slice } from './util'; import { mount } from './diff/mount'; +import { createInternal } from './tree'; /** * Render a Preact virtual node into a DOM element @@ -28,6 +29,7 @@ export function render(vnode, parentDom, replaceNode) { let oldVNode = isHydrating ? null : parentDom._children; vnode = parentDom._children = createElement(Fragment, null, [vnode]); + const internal = createInternal(oldVNode || vnode, null); // List of effects that need to be called after diffing. let commitQueue = [], @@ -51,9 +53,7 @@ export function render(vnode, parentDom, replaceNode) { } else { mount( parentDom, - // Determine the new vnode tree and store it on the DOM element on - // our custom `_children` property. - vnode, + internal, EMPTY_OBJ, parentDom.namespaceURI, parentDom.firstChild ? slice.call(parentDom.childNodes) : null, diff --git a/src/tree.js b/src/tree.js new file mode 100644 index 0000000000..32ac289075 --- /dev/null +++ b/src/tree.js @@ -0,0 +1,70 @@ +import { UNDEFINED } from './constants'; + +export const TYPE_TEXT = 1 << 0; +export const TYPE_ELEMENT = 1 << 1; +export const TYPE_CLASS = 1 << 2; +export const TYPE_FUNCTION = 1 << 3; +export const TYPE_INVALID = 1 << 6; +export const TYPE_COMPONENT = TYPE_CLASS | TYPE_FUNCTION; + +export const MODE_SVG = 1 << 4; +export const MODE_MATH = 1 << 5; +const INHERITED_MODES = MODE_MATH | MODE_SVG; + +/** + * + * @param {VNode} vnode + * @param {Internal | null} parentInternal + * @returns {Internal} + */ +export function createInternal(vnode, parentInternal) { + let flags = parentInternal ? parentInternal.flags & INHERITED_MODES : 0, + type = vnode.type; + + if (vnode.constructor !== UNDEFINED) { + flags |= TYPE_INVALID; + } else if (typeof vnode == 'string' || type == null) { + // type = null; + flags |= TYPE_TEXT; + } else { + // flags = typeof type === 'function' ? COMPONENT_NODE : ELEMENT_NODE; + flags |= + typeof type == 'function' + ? type.prototype && type.prototype.render + ? TYPE_CLASS + : TYPE_FUNCTION + : TYPE_ELEMENT; + + if (flags & TYPE_ELEMENT && type === 'svg') { + flags |= MODE_SVG; + } else if ( + parentInternal && + parentInternal.flags & MODE_SVG && + parentInternal.type === 'foreignObject' + ) { + flags &= ~MODE_SVG; + } else if (flags & TYPE_ELEMENT && type === 'math') { + flags |= MODE_MATH; + } + } + + return { + type, + props: vnode.props, + key: vnode.key, + ref: vnode.ref, + data: + flags & TYPE_COMPONENT + ? { _commitCallbacks: [], _context: null, _stateCallbacks: [] } + : null, + flags, + // @ts-expect-error + vnode, + // TODO: rerender + _children: null, + _parent: parentInternal, + _vnodeId: vnode._original, + _component: null, + _depth: parentInternal ? parentInternal._depth + 1 : 0 + }; +}