diff --git a/src/backing-node.js b/src/backing-node.js new file mode 100644 index 0000000000..651d91f2e1 --- /dev/null +++ b/src/backing-node.js @@ -0,0 +1,88 @@ +import { + TYPE_FUNCTION, + TYPE_ELEMENT, + TYPE_TEXT, + TYPE_CLASS, + TYPE_ROOT, + TYPE_COMPONENT, + MODE_SVG, + UNDEFINED +} from './constants'; +import { enqueueRenderInternal } from './component'; + +/** + * Create an internal tree node + * @param {VNode | string} vnode + * @param {Internal} [parentInternal] + * @returns {Internal} + */ +export function createInternal(vnode, parentInternal) { + let type = null, + props, + key, + ref; + + /** @type {number} */ + let flags = parentInternal ? parentInternal.flags : 0; + + // Text VNodes/Internals have an ID of 0 that is never used: + let vnodeId = 0; + + if (typeof vnode == 'string') { + // type = null; + flags |= TYPE_TEXT; + props = vnode; + } + // Prevent JSON injection by rendering injected objects as empty Text nodes + else if (vnode.constructor !== UNDEFINED) { + flags |= TYPE_TEXT; + props = ''; + } else { + type = vnode.type; + props = vnode.props; + key = vnode.key; + ref = vnode.ref; + vnodeId = vnode._original; + + flags |= + typeof type == 'function' + ? type.prototype && type.prototype.render + ? TYPE_CLASS + : props._parentDom + ? TYPE_ROOT + : TYPE_FUNCTION + : TYPE_ELEMENT; + + // TODO: add math mode + if (flags & TYPE_ELEMENT && type === 'svg') { + flags |= MODE_SVG; + } else if ( + parentInternal && + parentInternal.flags & MODE_SVG && + parentInternal.type === 'foreignObject' + ) { + flags &= ~MODE_SVG; + } + } + + /** @type {Internal} */ + const internal = { + type, + props, + key, + ref, + data: + flags & TYPE_COMPONENT + ? { _commitCallbacks: [], _context: null, _stateCallbacks: [] } + : null, + rerender: enqueueRenderInternal, + flags, + _children: null, + _parent: parentInternal, + _vnodeId: vnodeId, + _component: null, + _depth: parentInternal ? parentInternal._depth + 1 : 0 + }; + + return internal; +} diff --git a/src/component.js b/src/component.js index 4d9f3d608b..b083d054ea 100644 --- a/src/component.js +++ b/src/component.js @@ -2,7 +2,7 @@ import { assign } from './util'; import { diff, commitRoot } from './diff/index'; import options from './options'; import { Fragment } from './create-element'; -import { MODE_HYDRATE } from './constants'; +import { DIRTY_BIT, MODE_HYDRATE } from './constants'; /** * Base Component class. Provides `setState()` and `forceUpdate()`, which @@ -206,6 +206,23 @@ export function enqueueRender(c) { } } +/** + * Enqueue a rerender of a component + * @param {Internal} internal The component to rerender + */ +export function enqueueRenderInternal(internal) { + if ( + (!(internal.flags & DIRTY_BIT) && + (internal.flags |= DIRTY_BIT) && + rerenderQueue.push(internal) && + !process._rerenderCount++) || + prevDebounce !== options.debounceRendering + ) { + prevDebounce = options.debounceRendering; + (prevDebounce || queueMicrotask)(process); + } +} + /** * @param {Component} a * @param {Component} b diff --git a/src/constants.js b/src/constants.js index 3bcec6cfac..7909722e23 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,14 +1,69 @@ +// Internal.flags bitfield 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; +/** Signals this internal has a _parentDom prop that should change the parent + * DOM node of it's children */ +export const TYPE_ROOT = 1 << 4; + +/** Any type of internal representing DOM */ +export const TYPE_DOM = TYPE_TEXT | TYPE_ELEMENT; +/** Any type of component */ +export const TYPE_COMPONENT = TYPE_CLASS | TYPE_FUNCTION | TYPE_ROOT; + /** Normal hydration that attaches to a DOM tree but does not diff it. */ export const MODE_HYDRATE = 1 << 5; +/** Top level render unspecified behaviour (old replaceNode parameter to render) */ +export const MODE_MUTATIVE_HYDRATE = 1 << 6; /** Signifies this VNode suspended on the previous render */ export const MODE_SUSPENDED = 1 << 7; +/** Signifies this VNode errored on the previous render */ +export const MODE_ERRORED = 1 << 8; +/** Signifies an error has been thrown and this component will be attempting to + * handle & rerender the error on next render. In other words, on the next + * render of this component, unset this mode and set the MODE_RERENDERING_ERROR. + * This flag is distinct from MODE_RERENDERING_ERROR so that a component can + * catch multiple errors thrown by its children in one render pass (see test + * "should handle double child throws"). + */ +export const MODE_PENDING_ERROR = 1 << 9; +/** Signifies this Internal is attempting to "handle" an error and is + * rerendering. This mode tracks that a component's last rerender was trying to + * handle an error. As such, if another error is thrown while a component has + * this flag set, it should not handle the newly thrown error since it failed to + * successfully rerender the original error. This prevents error handling + * infinite render loops */ +export const MODE_RERENDERING_ERROR = 1 << 10; +/** Signals this internal has been unmounted */ +export const MODE_UNMOUNTING = 1 << 11; +/** This Internal is rendered in an SVG tree */ +export const MODE_SVG = 1 << 12; +/** Signifies that bailout checks will be bypassed */ +export const FORCE_UPDATE = 1 << 13; +/** Signifies that a node needs to be updated */ +export const DIRTY_BIT = 1 << 14; +/** Signals the component can skip children due to a non-update */ +export const SKIP_CHILDREN = 1 << 15; /** Indicates that this node needs to be inserted while patching children */ export const INSERT_VNODE = 1 << 16; /** Indicates a VNode has been matched with another VNode in the diff */ export const MATCHED = 1 << 17; +/** This Internal is rendered in a math tree */ +export const MODE_MATH = 1 << 18; /** Reset all mode flags */ -export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED); +export const RESET_MODE = ~( + MODE_HYDRATE | + MODE_MUTATIVE_HYDRATE | + MODE_SUSPENDED | + MODE_ERRORED | + MODE_RERENDERING_ERROR | + FORCE_UPDATE | + SKIP_CHILDREN +); + +export const UNDEFINED = undefined; export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; diff --git a/src/create-element.js b/src/create-element.js index 66898b2224..fb68d75677 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -62,20 +62,8 @@ export function createVNode(type, props, key, ref, original) { props, key, ref, - _children: null, - _parent: null, - _depth: 0, - _dom: null, - // _nextDom must be initialized to undefined b/c it will eventually - // be set to dom.nextSibling which can return `null` and it is important - // to be able to distinguish between an uninitialized _nextDom and - // a _nextDom that has been set to `null` - _nextDom: undefined, - _component: null, constructor: undefined, - _original: original == null ? ++vnodeId : original, - _index: -1, - _flags: 0 + _original: original == null ? ++vnodeId : original }; // Only invoke the vnode hook if this was *not* a direct copy: diff --git a/src/internal.d.ts b/src/internal.d.ts index b4fa3fd7c3..a3b899f00d 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -84,6 +84,52 @@ declare global { contextType?: PreactContext; } + export interface VNode
extends preact.VNode
{ + // Redefine type here using our internal ComponentType type + type: string | ComponentType
; + props: P & { children: ComponentChildren }; + /** + * Internal GUID for this VNode, used for fast equality checks. + * Note: h() allocates monotonic positive integer IDs, jsx() allocates negative. + * @private + */ + _original: 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 = ComponentClass | FunctionComponent ;
@@ -121,7 +167,7 @@ declare global {
checked?: HTMLInputElement['checked'];
// Internal properties
- _children?: VNode ;
props: P & { children: ComponentChildren };
ref?: Ref extends preact.Component {
diff --git a/src/render.js b/src/render.js
index 17f1220b3e..c086e9dd85 100644
--- a/src/render.js
+++ b/src/render.js
@@ -3,6 +3,7 @@ import { commitRoot, diff } from './diff/index';
import { createElement, Fragment } from './create-element';
import options from './options';
import { slice } from './util';
+import { createInternal } from './backing-node';
/**
* Render a Preact virtual node into a DOM element
@@ -26,7 +27,8 @@ export function render(vnode, parentDom, replaceNode) {
// means that we are mounting a new tree for the first time.
let oldVNode = isHydrating ? null : parentDom._children;
- vnode = parentDom._children = createElement(Fragment, null, [vnode]);
+ vnode = createElement(Fragment, null, [vnode]);
+ const rootInternal = (parentDom._children = createInternal(vnode));
// List of effects that need to be called after diffing.
let commitQueue = [],