Skip to content

Commit

Permalink
Add internals for mount path
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Dec 7, 2024
1 parent 9809be6 commit 948205b
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 56 deletions.
5 changes: 4 additions & 1 deletion src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,9 +111,11 @@ export function diffChildren(
refQueue
);
} else {
// TODO: temp
const internal = createInternal(childVNode, null);
result = mount(
parentDom,
childVNode,
internal,
globalContext,
namespace,
excessDomChildren,
Expand Down
118 changes: 75 additions & 43 deletions src/diff/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* @param {Array<import('../internal').PreactElement>} excessDomChildren
* @param {Array<import('../internal').Component>} 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`.
Expand All @@ -31,7 +42,7 @@ import options from '../options';
*/
export function mount(
parentDom,
newVNode,
internal,
globalContext,
namespace,
excessDomChildren,
Expand All @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -156,6 +177,7 @@ export function mount(
let renderResult = isTopLevelFragment ? tmp.props.children : tmp;

oldDom = mountChildren(
internal,
parentDom,
isArray(renderResult) ? renderResult : [renderResult],
newVNode,
Expand Down Expand Up @@ -200,7 +222,7 @@ export function mount(
}
} else {
oldDom = newVNode._dom = mountElementNode(
newVNode,
internal,
globalContext,
namespace,
excessDomChildren,
Expand All @@ -217,43 +239,45 @@ 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<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* @param {Array<import('../internal').PreactElement>} excessDomChildren
* @param {Array<import('../internal').Component>} 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,
commitQueue,
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';

Expand All @@ -277,7 +301,7 @@ function mountElementNode(
}

if (dom == null) {
if (nodeType === null) {
if (internal.flags & TYPE_TEXT) {
return document.createTextNode(newProps);
}

Expand All @@ -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 {
Expand Down Expand Up @@ -361,6 +385,7 @@ function mountElementNode(
newVNode._children = [];
} else {
mountChildren(
internal,
dom,
isArray(newChildren) ? newChildren : [newChildren],
newVNode,
Expand Down Expand Up @@ -416,25 +441,27 @@ 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<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* @param {Array<import('../internal').PreactElement>} excessDomChildren
* @param {Array<import('../internal').Component>} 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`.
* @param {boolean} isHydrating Whether or not we are in hydration
* @param {any[]} refQueue an array of elements needed to invoke refs
*/
function mountChildren(
internal,
parentDom,
renderResult,
newParentVNode,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions src/diff/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 || [];
Expand Down
40 changes: 37 additions & 3 deletions src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<P = {}> {
type: string | ComponentType<P>;
/** The props object for Elements/Components, and the string contents for Text */
props: (P & { children: ComponentChildren }) | string | number;
key: any;
ref: Ref<any> | 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;
Expand Down Expand Up @@ -62,8 +98,7 @@ export type ComponentChild =
| undefined;
export type ComponentChildren = ComponentChild[] | ComponentChild;

export interface FunctionComponent<P = {}>
extends preact.FunctionComponent<P> {
export interface FunctionComponent<P = {}> extends preact.FunctionComponent<P> {
// Internally, createContext uses `contextType` on a Function component to
// implement the Consumer component
contextType?: PreactContext;
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 948205b

Please sign in to comment.