diff --git a/src/diff/children.js b/src/diff/children.js index 98437e75d4..dc7befaaa5 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'; /** * @typedef {import('../internal').ComponentChildren} ComponentChildren @@ -110,9 +111,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..e9471ef99b 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -11,18 +11,29 @@ 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 {import('../internal').PreactElement} parentDom The parent of the DOM element + * @param {import('../internal').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) - * @param {Array} excessDomChildren - * @param {Array} commitQueue List of components which have callbacks + * @param {Array} excessDomChildren + * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot - * @param {PreactElement} oldDom The current attached DOM element any new dom + * @param {import('../internal').PreactElement} oldDom The current attached DOM element any new dom * elements should be placed around. Likely `null` on first render (except when * hydrating). Can be a sibling DOM element when diffing Fragments that have * siblings. In most cases, it starts out as `oldChildren[0]._dom`. @@ -31,7 +42,7 @@ import options from '../options'; */ export function mount( parentDom, - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -40,22 +51,26 @@ 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 {import('../internal').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 +84,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 +177,7 @@ export function mount( let renderResult = isTopLevelFragment ? tmp.props.children : tmp; oldDom = mountChildren( + internal, parentDom, isArray(renderResult) ? renderResult : [renderResult], newVNode, @@ -200,7 +222,7 @@ export function mount( } } else { oldDom = newVNode._dom = mountElementNode( - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -217,18 +239,18 @@ export function mount( /** * Diff two virtual nodes representing DOM element - * @param {VNode} newVNode The new virtual node + * @param {import('../internal').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 - * @param {Array} commitQueue List of components which have callbacks + * @param {Array} excessDomChildren + * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot * @param {boolean} isHydrating Whether or not we are in hydration * @param {any[]} refQueue an array of elements needed to invoke refs - * @returns {PreactElement} + * @returns {import('../internal').PreactElement} */ function mountElementNode( - newVNode, + internal, globalContext, namespace, excessDomChildren, @@ -236,24 +258,26 @@ function mountElementNode( isHydrating, refQueue ) { - /** @type {PreactElement} */ + // @ts-expect-error + const newVNode = internal.vnode; + /** @type {import('../internal').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 }} */ let newHtml; - /** @type {ComponentChildren} */ + /** @type {import('../internal').ComponentChildren} */ let newChildren; let value; let inputValue; 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 +301,7 @@ function mountElementNode( } if (dom == null) { - if (nodeType === null) { + if (internal.flags & TYPE_TEXT) { return document.createTextNode(newProps); } @@ -298,7 +322,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 +385,7 @@ function mountElementNode( newVNode._children = []; } else { mountChildren( + internal, dom, isArray(newChildren) ? newChildren : [newChildren], newVNode, @@ -416,18 +441,19 @@ function doRender(props, _state, context) { /** * Diff the children of a virtual node - * @param {PreactElement} parentDom The DOM element whose children are being + * @param {import('../internal').Internal} internal The DOM element whose children are being + * @param {import('../internal').PreactElement} parentDom The DOM element whose children are being * diffed - * @param {ComponentChildren[]} renderResult - * @param {VNode} newParentVNode The new virtual node whose children should be + * @param {import('../internal').ComponentChildren[]} renderResult + * @param {import('../internal').VNode} newParentVNode The new virtual node whose children should be * diff'ed against oldParentVNode * @param {object} globalContext The current context object - modified by * getChildContext * @param {string} namespace Current namespace of the DOM node (HTML, SVG, or MathML) - * @param {Array} excessDomChildren - * @param {Array} commitQueue List of components which have callbacks + * @param {Array} excessDomChildren + * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot - * @param {PreactElement} oldDom The current attached DOM element any new dom + * @param {import('../internal').PreactElement} oldDom The current attached DOM element any new dom * elements should be placed around. Likely `null` on first render (except when * hydrating). Can be a sibling DOM element when diffing Fragments that have * siblings. In most cases, it starts out as `oldChildren[0]._dom`. @@ -435,6 +461,7 @@ function doRender(props, _state, context) { * @param {any[]} refQueue an array of elements needed to invoke refs */ function mountChildren( + internal, parentDom, renderResult, newParentVNode, @@ -447,11 +474,11 @@ function mountChildren( refQueue ) { let i, - /** @type {VNode} */ + /** @type {import('../internal').VNode} */ childVNode, - /** @type {PreactElement} */ + /** @type {import('../internal').PreactElement} */ newDom, - /** @type {PreactElement} */ + /** @type {import('../internal').PreactElement} */ firstChildDom; let newChildrenLength = renderResult.length; @@ -517,10 +544,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 +572,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/diff/operations.js b/src/diff/operations.js index 2369ca4b3c..948091697d 100644 --- a/src/diff/operations.js +++ b/src/diff/operations.js @@ -2,10 +2,10 @@ import { getDomSibling } from '../component'; import { isArray } from '../util'; /** - * @param {VNode} parentVNode - * @param {PreactElement} oldDom - * @param {PreactElement} parentDom - * @returns {PreactElement} + * @param {import('../internal').VNode} parentVNode + * @param {import('../internal').PreactElement} oldDom + * @param {import('../internal').PreactElement} parentDom + * @returns {import('../internal').PreactElement} */ export function insert(parentVNode, oldDom, parentDom) { // Note: VNodes in nested suspended trees may be missing _children. @@ -41,9 +41,9 @@ export function insert(parentVNode, oldDom, parentDom) { /** * Flatten and loop through the children of a virtual node - * @param {ComponentChildren} children The unflattened children of a virtual + * @param {import('../internal').ComponentChildren} children The unflattened children of a virtual * node - * @returns {VNode[]} + * @returns {import('../internal').VNode[]} */ export function toChildArray(children, out) { out = out || []; diff --git a/src/internal.d.ts b/src/internal.d.ts index b002ff449f..0a6f898255 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -16,6 +16,42 @@ export enum HookType { // Not a real hook, but the devtools treat is as such useDebugvalue = 11 } +/** + * 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 DevSource { fileName: string; @@ -62,8 +98,7 @@ export type ComponentChild = | undefined; export type ComponentChildren = ComponentChild[] | ComponentChild; -export interface FunctionComponent

- extends preact.FunctionComponent

{ +export interface FunctionComponent

extends preact.FunctionComponent

{ // Internally, createContext uses `contextType` on a Function component to // implement the Consumer component contextType?: PreactContext; @@ -113,7 +148,6 @@ export interface PreactElement extends preact.ContainerNode { readonly nextSibling: ContainerNode | null; readonly firstChild: ContainerNode | null; - // Used to match DOM nodes to VNodes during hydration. Note: doesn't exist // on Text nodes readonly localName?: string; diff --git a/src/render.js b/src/render.js index 86b3b10136..668295f14f 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 @@ -33,6 +34,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 = [], @@ -56,9 +58,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..f9d26be75e --- /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 {import('./internal').VNode} vnode + * @param {import('./internal').Internal | null} parentInternal + * @returns {import('./internal').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 + }; +}