;
- _commitQueue: CommitQueue;
- _parentDom: Element | Document | ShadowRoot | DocumentFragment;
-};
-
export type CommitQueue = Internal[];
+export type DOMParent = Element | Document | ShadowRoot | DocumentFragment;
+
// Redefine ComponentFactory using our new internal FunctionalComponent interface above
export type ComponentFactory =
| preact.ComponentClass
From 09d4126c50d159e0b0516615859555dc43870bef Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Tue, 19 Apr 2022 11:35:25 -0400
Subject: [PATCH 02/14] Checkpoint: switch parentDom back to an argument,
context back to conditional tree walk.
---
src/component.js | 14 ++-------
src/create-root.js | 13 ++------
src/diff/children.js | 21 ++++++++-----
src/diff/component.js | 35 +++++++++++-----------
src/diff/mount.js | 70 ++++++++++++++++++++-----------------------
src/diff/patch.js | 57 +++++++++++++++--------------------
src/diff/renderer.js | 27 +----------------
7 files changed, 94 insertions(+), 143 deletions(-)
diff --git a/src/component.js b/src/component.js
index 22e9dc8e64..719e42b7bb 100644
--- a/src/component.js
+++ b/src/component.js
@@ -1,13 +1,9 @@
-import {
- commitRoot,
- setCurrentContext,
- setCurrentParentDom
-} from './diff/renderer';
+import { commitRoot } from './diff/renderer';
import options from './options';
import { createVNode, Fragment } from './create-element';
import { patch } from './diff/patch';
import { DIRTY_BIT, FORCE_UPDATE, MODE_UNMOUNTING } from './constants';
-import { getParentContext, getParentDom } from './tree';
+import { getParentDom } from './tree';
/**
* Base Component class. Provides `setState()` and `forceUpdate()`, which
@@ -102,11 +98,7 @@ function rerender(internal) {
0
);
- // set up renderer state
- setCurrentContext(getParentContext(internal));
- setCurrentParentDom(getParentDom(internal));
-
- patch(internal, vnode);
+ patch(internal, vnode, getParentDom(internal));
commitRoot(internal);
}
}
diff --git a/src/create-root.js b/src/create-root.js
index 2edfe47a96..58f8c5f9a6 100644
--- a/src/create-root.js
+++ b/src/create-root.js
@@ -4,11 +4,7 @@ import {
MODE_SVG,
UNDEFINED
} from './constants';
-import {
- commitRoot,
- setCurrentContext,
- setCurrentParentDom
-} from './diff/renderer';
+import { commitRoot } from './diff/renderer';
import { createElement, Fragment } from './create-element';
import options from './options';
import { mount } from './diff/mount';
@@ -33,11 +29,8 @@ export function createRoot(parentDom) {
firstChild =
/** @type {import('./internal').PreactElement} */ (parentDom.firstChild);
- setCurrentContext({});
- setCurrentParentDom(parentDom);
-
if (rootInternal) {
- patch(rootInternal, vnode);
+ patch(rootInternal, vnode, parentDom);
} else {
rootInternal = createInternal(vnode);
@@ -56,7 +49,7 @@ export function createRoot(parentDom) {
rootInternal._context = {};
- mount(rootInternal, vnode, firstChild);
+ mount(rootInternal, vnode, parentDom, firstChild);
}
// Flush all queued effects
diff --git a/src/diff/children.js b/src/diff/children.js
index 0d5edd837f..40d06ac852 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -12,14 +12,14 @@ import { mount } from './mount';
import { patch } from './patch';
import { unmount } from './unmount';
import { createInternal, getDomSibling } from '../tree';
-import { getCurrentParentDom } from './renderer';
/**
* Update an internal with new children.
* @param {import('../internal').Internal} internal The internal whose children should be patched
* @param {import('../internal').ComponentChild[]} children The new children, represented as VNodes
+ * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered
*/
-export function patchChildren(internal, children) {
+export function patchChildren(internal, children, parentDom) {
let oldChildren =
(internal._children && internal._children.slice()) || EMPTY_ARR;
@@ -72,7 +72,12 @@ export function patchChildren(internal, children) {
childInternal = createInternal(childVNode, internal);
// We are mounting a new VNode
- mount(childInternal, childVNode, getDomSibling(internal, skewedIndex));
+ mount(
+ childInternal,
+ childVNode,
+ parentDom,
+ getDomSibling(internal, skewedIndex)
+ );
}
// If this node suspended during hydration, and no other flags are set:
// @TODO: might be better to explicitly check for MODE_ERRORED here.
@@ -81,10 +86,10 @@ export function patchChildren(internal, children) {
(MODE_HYDRATE | MODE_SUSPENDED)
) {
// We are resuming the hydration of a VNode
- mount(childInternal, childVNode, childInternal._dom);
+ mount(childInternal, childVNode, parentDom, childInternal._dom);
} else {
// Morph the old element into the new one, but don't append it to the dom yet
- patch(childInternal, childVNode);
+ patch(childInternal, childVNode, parentDom);
}
go: if (mountingChild) {
@@ -94,7 +99,7 @@ export function patchChildren(internal, children) {
// Perform insert of new dom
if (childInternal.flags & TYPE_DOM) {
- getCurrentParentDom().insertBefore(
+ parentDom.insertBefore(
childInternal._dom,
getDomSibling(internal, skewedIndex)
);
@@ -128,9 +133,9 @@ export function patchChildren(internal, children) {
let nextSibling = getDomSibling(internal, skewedIndex + 1);
if (childInternal.flags & TYPE_DOM) {
- getCurrentParentDom().insertBefore(childInternal._dom, nextSibling);
+ parentDom.insertBefore(childInternal._dom, nextSibling);
} else {
- insertComponentDom(childInternal, nextSibling, getCurrentParentDom());
+ insertComponentDom(childInternal, nextSibling, parentDom);
}
}
diff --git a/src/diff/component.js b/src/diff/component.js
index eff6cacc47..e6d226c48c 100644
--- a/src/diff/component.js
+++ b/src/diff/component.js
@@ -1,14 +1,20 @@
import options from '../options';
import { DIRTY_BIT, FORCE_UPDATE, SKIP_CHILDREN } from '../constants';
-import { getCurrentContext, setCurrentContext } from './renderer';
/**
* Render a function component
* @param {import('../internal').Internal} internal The component's backing Internal node
* @param {import('../internal').VNode} newVNode The new virtual node
+ * @param {any} context Full context object from the nearest ancestor component Internal
+ * @param {any} componentContext Scoped/selected context for this component
* @returns {import('../internal').ComponentChildren} the component's children
*/
-export function renderFunctionComponent(internal, newVNode, componentContext) {
+export function renderFunctionComponent(
+ internal,
+ newVNode,
+ context,
+ componentContext
+) {
/** @type {import('../internal').Component} */
let c;
@@ -49,13 +55,7 @@ export function renderFunctionComponent(internal, newVNode, componentContext) {
}
internal.flags &= ~DIRTY_BIT;
if (c.getChildContext != null) {
- setCurrentContext(
- (internal._context = Object.assign(
- {},
- getCurrentContext(),
- c.getChildContext()
- ))
- );
+ internal._context = Object.assign({}, context, c.getChildContext());
}
return renderResult;
@@ -65,9 +65,16 @@ export function renderFunctionComponent(internal, newVNode, componentContext) {
* Render a class component
* @param {import('../internal').Internal} internal The component's backing Internal node
* @param {import('../internal').VNode} newVNode The new virtual node
+ * @param {any} context Full context object from the nearest ancestor component Internal
+ * @param {any} componentContext Scoped/selected context for this component
* @returns {import('../internal').ComponentChildren} the component's children
*/
-export function renderClassComponent(internal, newVNode, componentContext) {
+export function renderClassComponent(
+ internal,
+ newVNode,
+ context,
+ componentContext
+) {
/** @type {import('../internal').Component} */
let c;
let isNew, oldProps, oldState, snapshot;
@@ -157,13 +164,7 @@ export function renderClassComponent(internal, newVNode, componentContext) {
c.state = c._nextState;
if (c.getChildContext != null) {
- setCurrentContext(
- (internal._context = Object.assign(
- {},
- getCurrentContext(),
- c.getChildContext()
- ))
- );
+ internal._context = Object.assign({}, context, c.getChildContext());
}
if (!isNew) {
diff --git a/src/diff/mount.js b/src/diff/mount.js
index 734cbc23ca..ca9e4d9996 100644
--- a/src/diff/mount.js
+++ b/src/diff/mount.js
@@ -12,27 +12,22 @@ import {
TYPE_ROOT,
MODE_SVG
} from '../constants';
+import options from '../options';
import { normalizeToVNode, Fragment } from '../create-element';
import { setProperty } from './props';
import { renderClassComponent, renderFunctionComponent } from './component';
-import { createInternal } from '../tree';
-import options from '../options';
-import {
- commitQueue,
- getCurrentContext,
- getCurrentParentDom,
- setCurrentContext,
- setCurrentParentDom
-} from './renderer';
+import { createInternal, getParentContext } from '../tree';
+import { commitQueue } from './renderer';
/**
* Diff two virtual nodes and apply proper changes to the DOM
* @param {import('../internal').Internal} internal The Internal node to mount
* @param {import('../internal').VNode | string} newVNode The new virtual node
+ * @param {import('../internal').PreactElement} parentDom The element into which this subtree should be inserted
* @param {import('../internal').PreactNode} startDom
* @returns {import('../internal').PreactNode | null} pointer to the next DOM node to be hydrated (or null)
*/
-export function mount(internal, newVNode, startDom) {
+export function mount(internal, newVNode, parentDom, startDom) {
if (options._diff) options._diff(internal, newVNode);
/** @type {import('../internal').PreactNode} */
@@ -44,38 +39,44 @@ export function mount(internal, newVNode, startDom) {
// the page. Root nodes can occur anywhere in the tree and not just at the
// top.
let prevStartDom = startDom;
- let prevParentDom = getCurrentParentDom();
+ let prevParentDom = parentDom;
if (internal.flags & TYPE_ROOT) {
- let newParentDom = newVNode.props._parentDom;
- setCurrentParentDom(newParentDom);
+ parentDom = newVNode.props._parentDom;
// Note: this is likely always true because we are inside mount()
- if (newParentDom !== prevParentDom) {
+ if (parentDom !== prevParentDom) {
startDom = null;
}
}
- let prevContext = getCurrentContext();
+ let context = getParentContext(internal);
+
// Necessary for createContext api. Setting this property will pass
// the context value as `this.context` just for this component.
let tmp = newVNode.type.contextType;
- let provider = tmp && prevContext[tmp._id];
+ let provider = tmp && context[tmp._id];
let componentContext = tmp
? provider
? provider.props.value
: tmp._defaultValue
- : prevContext;
+ : context;
if (provider) provider._subs.add(internal);
let renderResult;
if (internal.flags & TYPE_CLASS) {
- renderResult = renderClassComponent(internal, null, componentContext);
+ renderResult = renderClassComponent(
+ internal,
+ null,
+ context,
+ componentContext
+ );
} else {
renderResult = renderFunctionComponent(
internal,
null,
+ context,
componentContext
);
}
@@ -95,20 +96,19 @@ export function mount(internal, newVNode, startDom) {
renderResult = [renderResult];
}
- nextDomSibling = mountChildren(internal, renderResult, startDom);
+ nextDomSibling = mountChildren(
+ internal,
+ renderResult,
+ parentDom,
+ startDom
+ );
}
- if (
- internal._commitCallbacks != null &&
- internal._commitCallbacks.length
- ) {
+ if (internal._commitCallbacks.length) {
commitQueue.push(internal);
}
- if (
- internal.flags & TYPE_ROOT &&
- prevParentDom !== getCurrentParentDom()
- ) {
+ if (internal.flags & TYPE_ROOT && prevParentDom !== parentDom) {
// If we just mounted a root node/Portal, and it changed the parentDom
// of it's children, then we need to resume the diff from it's previous
// startDom element, which could be null if we are mounting an entirely
@@ -116,11 +116,6 @@ export function mount(internal, newVNode, startDom) {
// an existing tree.
nextDomSibling = prevStartDom;
}
-
- setCurrentParentDom(prevParentDom);
- // In the event this subtree creates a new context for its children, restore
- // the previous context for its siblings
- setCurrentContext(prevContext);
} else {
// @TODO: we could just assign this as internal.dom here
let hydrateDom =
@@ -269,14 +264,12 @@ function mountElement(internal, dom) {
dom.innerHTML = newHtml.__html;
}
} else if (newChildren != null) {
- const prevParentDom = getCurrentParentDom();
- setCurrentParentDom(dom);
mountChildren(
internal,
Array.isArray(newChildren) ? newChildren : [newChildren],
+ dom,
isNew ? null : dom.firstChild
);
- setCurrentParentDom(prevParentDom);
}
// (as above, don't diff props during hydration)
@@ -293,9 +286,10 @@ function mountElement(internal, dom) {
* Mount all children of an Internal
* @param {import('../internal').Internal} internal The parent Internal of the given children
* @param {import('../internal').ComponentChild[]} children
+ * @param {import('../internal').PreactElement} parentDom The element into which this subtree should be inserted
* @param {import('../internal').PreactNode} startDom
*/
-export function mountChildren(internal, children, startDom) {
+export function mountChildren(internal, children, parentDom, startDom) {
let internalChildren = (internal._children = []),
i,
childVNode,
@@ -317,7 +311,7 @@ export function mountChildren(internal, children, startDom) {
internalChildren[i] = childInternal;
// Morph the old element into the new one, but don't append it to the dom yet
- mountedNextChild = mount(childInternal, childVNode, startDom);
+ mountedNextChild = mount(childInternal, childVNode, parentDom, startDom);
newDom = childInternal._dom;
@@ -330,7 +324,7 @@ export function mountChildren(internal, children, startDom) {
// The DOM the diff should begin with is now startDom (since we inserted
// newDom before startDom) so ignore mountedNextChild and continue with
// startDom
- getCurrentParentDom().insertBefore(newDom, startDom);
+ parentDom.insertBefore(newDom, startDom);
}
if (childInternal.ref) {
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 344b8a8e78..a2922fb39e 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -18,23 +18,18 @@ import {
SKIP_CHILDREN,
DIRTY_BIT
} from '../constants';
-import { getDomSibling } from '../tree';
+import { getDomSibling, getParentContext } from '../tree';
import { mountChildren } from './mount';
import { Fragment } from '../create-element';
-import {
- commitQueue,
- getCurrentContext,
- getCurrentParentDom,
- setCurrentContext,
- setCurrentParentDom
-} from './renderer';
+import { commitQueue } from './renderer';
/**
* Diff two virtual nodes and apply proper changes to the DOM
* @param {import('../internal').Internal} internal The Internal node to patch
* @param {import('../internal').VNode | string} vnode The new virtual node
+ * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered
*/
-export function patch(internal, vnode) {
+export function patch(internal, vnode, parentDom) {
let flags = internal.flags;
if (flags & TYPE_TEXT) {
@@ -56,15 +51,14 @@ export function patch(internal, vnode) {
// Root nodes render their children into a specific parent DOM element.
// They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
// @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
- let prevParentDom = getCurrentParentDom();
+ let prevParentDom = parentDom;
if (flags & TYPE_ROOT) {
- let newParentDom = vnode.props._parentDom;
- setCurrentParentDom(newParentDom);
+ parentDom = vnode.props._parentDom;
if (internal.props._parentDom !== vnode.props._parentDom) {
let nextSibling =
- newParentDom == prevParentDom ? getDomSibling(internal) : null;
- insertComponentDom(internal, nextSibling, newParentDom);
+ parentDom == prevParentDom ? getDomSibling(internal) : null;
+ insertComponentDom(internal, nextSibling, parentDom);
}
}
@@ -82,26 +76,33 @@ export function patch(internal, vnode) {
internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
}
- let prevContext = getCurrentContext();
+ let context = getParentContext(internal);
+
// Necessary for createContext api. Setting this property will pass
// the context value as `this.context` just for this component.
let tmp = vnode.type.contextType;
- let provider = tmp && prevContext[tmp._id];
+ let provider = tmp && context[tmp._id];
let componentContext = tmp
? provider
? provider.props.value
: tmp._defaultValue
- : prevContext;
+ : context;
let isNew = !internal || !internal._component;
let renderResult;
if (internal.flags & TYPE_CLASS) {
- renderResult = renderClassComponent(internal, vnode, componentContext);
+ renderResult = renderClassComponent(
+ internal,
+ vnode,
+ context,
+ componentContext
+ );
} else {
renderResult = renderFunctionComponent(
internal,
vnode,
+ context,
componentContext
);
}
@@ -135,22 +136,14 @@ export function patch(internal, vnode) {
? null
: getDomSibling(internal);
- mountChildren(internal, renderResult, siblingDom);
+ mountChildren(internal, renderResult, parentDom, siblingDom);
} else {
- patchChildren(internal, renderResult);
+ patchChildren(internal, renderResult, parentDom);
}
- if (
- internal._commitCallbacks != null &&
- internal._commitCallbacks.length
- ) {
+ if (internal._commitCallbacks.length) {
commitQueue.push(internal);
}
-
- setCurrentParentDom(prevParentDom);
- // In the event this subtree creates a new context for its children, restore
- // the previous context for its siblings
- setCurrentContext(prevContext);
} catch (e) {
// @TODO: assign a new VNode ID here? Or NaN?
// newVNode._vnodeId = 0;
@@ -222,13 +215,11 @@ function patchElement(internal, vnode) {
internal._children = null;
} else {
if (oldHtml) dom.innerHTML = '';
- const prevParentDom = getCurrentParentDom();
- setCurrentParentDom(dom);
patchChildren(
internal,
- newChildren && Array.isArray(newChildren) ? newChildren : [newChildren]
+ newChildren && Array.isArray(newChildren) ? newChildren : [newChildren],
+ dom
);
- setCurrentParentDom(prevParentDom);
}
if (newProps.checked != null && dom._isControlled) {
diff --git a/src/diff/renderer.js b/src/diff/renderer.js
index 4a53f0e63b..78c43fe621 100644
--- a/src/diff/renderer.js
+++ b/src/diff/renderer.js
@@ -1,41 +1,16 @@
import options from '../options';
-/**
- * The full context storage object for the Internal currently being rendered.
- * @type {Record}
- */
-let currentContext = {};
-export function getCurrentContext() {
- return currentContext;
-}
-export function setCurrentContext(context) {
- currentContext = context;
-}
-
/**
* A list of components with effects that need to be run at the end of the current render pass.
* @type {import('../internal').CommitQueue}
*/
export let commitQueue = [];
-/**
- * The parent DOM element for the Internal currently being rendered.
- * @type {import('../internal').DOMParent}
- */
-let parentDom;
-export function getCurrentParentDom() {
- return parentDom;
-}
-/** @param {import('../internal').DOMParent} newParentDom */
-export function setCurrentParentDom(newParentDom) {
- parentDom = newParentDom;
-}
-
/**
* @param {import('../internal').Internal} rootInternal
*/
export function commitRoot(rootInternal) {
- let currentQueue = [].concat(commitQueue);
+ let currentQueue = commitQueue;
commitQueue = [];
if (options._commit) options._commit(rootInternal, currentQueue);
From 475f051ee9dde11488a3644975781bdaf636e02c Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Thu, 21 Apr 2022 17:54:02 -0400
Subject: [PATCH 03/14] Experiment: separate component mount/patch, and
(re)unify Fn+Class components.
---
src/diff/mount.js | 476 ++++++++++++++++++++++++++--------------------
src/diff/patch.js | 307 +++++++++++++++++++-----------
2 files changed, 471 insertions(+), 312 deletions(-)
diff --git a/src/diff/mount.js b/src/diff/mount.js
index ca9e4d9996..829e4a5500 100644
--- a/src/diff/mount.js
+++ b/src/diff/mount.js
@@ -10,151 +10,233 @@ import {
TYPE_CLASS,
MODE_ERRORED,
TYPE_ROOT,
- MODE_SVG
+ MODE_SVG,
+ DIRTY_BIT
} from '../constants';
import options from '../options';
import { normalizeToVNode, Fragment } from '../create-element';
import { setProperty } from './props';
-import { renderClassComponent, renderFunctionComponent } from './component';
import { createInternal, getParentContext } from '../tree';
import { commitQueue } from './renderer';
/**
* Diff two virtual nodes and apply proper changes to the DOM
* @param {import('../internal').Internal} internal The Internal node to mount
- * @param {import('../internal').VNode | string} newVNode The new virtual node
+ * @param {import('../internal').VNode | string} vnode The new virtual node
* @param {import('../internal').PreactElement} parentDom The element into which this subtree should be inserted
* @param {import('../internal').PreactNode} startDom
* @returns {import('../internal').PreactNode | null} pointer to the next DOM node to be hydrated (or null)
*/
-export function mount(internal, newVNode, parentDom, startDom) {
- if (options._diff) options._diff(internal, newVNode);
+export function mount(internal, vnode, parentDom, startDom) {
+ if (options._diff) options._diff(internal, vnode);
+
+ let flags = internal.flags;
+ let props = internal.props;
/** @type {import('../internal').PreactNode} */
let nextDomSibling;
- try {
- if (internal.flags & TYPE_COMPONENT) {
- // Root nodes signal that an attempt to render into a specific DOM node on
- // the page. Root nodes can occur anywhere in the tree and not just at the
- // top.
- let prevStartDom = startDom;
- let prevParentDom = parentDom;
- if (internal.flags & TYPE_ROOT) {
- parentDom = newVNode.props._parentDom;
-
- // Note: this is likely always true because we are inside mount()
- if (parentDom !== prevParentDom) {
- startDom = null;
- }
- }
-
- let context = getParentContext(internal);
-
- // Necessary for createContext api. Setting this property will pass
- // the context value as `this.context` just for this component.
- let tmp = newVNode.type.contextType;
- let provider = tmp && context[tmp._id];
- let componentContext = tmp
- ? provider
- ? provider.props.value
- : tmp._defaultValue
- : context;
-
- if (provider) provider._subs.add(internal);
-
- let renderResult;
-
- if (internal.flags & TYPE_CLASS) {
- renderResult = renderClassComponent(
- internal,
- null,
- context,
- componentContext
- );
- } else {
- renderResult = renderFunctionComponent(
- internal,
- null,
- context,
- componentContext
- );
- }
+ // @TODO: could just assign this as internal.dom here?
+ let hydrateDom =
+ flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE) ? startDom : null;
+
+ // Root nodes signal that an attempt to render into a specific DOM node on
+ // the page. Root nodes can occur anywhere in the tree and not just at the
+ // top.
+ let prevStartDom;
+ let prevParentDom = parentDom;
+ if (flags & TYPE_ROOT) {
+ parentDom = props._parentDom;
+
+ if (parentDom !== prevParentDom) {
+ prevStartDom = startDom;
+ startDom = null;
+ }
+ }
- if (renderResult == null) {
- nextDomSibling = startDom;
- } else {
- if (typeof renderResult === 'object') {
- // dissolve unkeyed root fragments:
- if (renderResult.type === Fragment && renderResult.key == null) {
- renderResult = renderResult.props.children;
- }
- if (!Array.isArray(renderResult)) {
- renderResult = [renderResult];
- }
- } else {
- renderResult = [renderResult];
+ if (flags & TYPE_TEXT) {
+ // if hydrating (hydrate() or render() with replaceNode), find the matching child:
+ while (hydrateDom) {
+ nextDomSibling = hydrateDom.nextSibling;
+ if (hydrateDom.nodeType === 3) {
+ // if hydrating a Text node, ensure its text content is correct:
+ if (hydrateDom.data != props) {
+ hydrateDom.data = props;
}
-
- nextDomSibling = mountChildren(
- internal,
- renderResult,
- parentDom,
- startDom
- );
+ break;
}
+ hydrateDom = nextDomSibling;
+ }
+
+ // @ts-ignore createTextNode returns Text, we expect PreactElement
+ internal._dom = hydrateDom || document.createTextNode(props);
+ internal.flags &= RESET_MODE;
+ } else if (flags & TYPE_ELEMENT) {
+ nextDomSibling = mountElement(internal, hydrateDom);
+ internal.flags &= RESET_MODE;
+ } else {
+ try {
+ nextDomSibling = mountComponent(
+ internal,
+ props,
+ parentDom,
+ startDom,
+ flags
+ );
if (internal._commitCallbacks.length) {
commitQueue.push(internal);
}
- if (internal.flags & TYPE_ROOT && prevParentDom !== parentDom) {
- // If we just mounted a root node/Portal, and it changed the parentDom
- // of it's children, then we need to resume the diff from it's previous
- // startDom element, which could be null if we are mounting an entirely
- // new tree, or the portal's nextSibling if we are mounting a Portal in
- // an existing tree.
- nextDomSibling = prevStartDom;
+ // We successfully rendered this VNode, unset any stored hydration/bailout state:
+ internal.flags &= RESET_MODE;
+ } catch (e) {
+ internal._vnodeId = 0;
+ internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
+
+ if (flags & MODE_HYDRATE) {
+ // @ts-ignore Trust me TS, nextSibling is a PreactElement
+ nextDomSibling = startDom && startDom.nextSibling;
+ internal._dom = startDom; // Save our current DOM position to resume later
}
- } else {
- // @TODO: we could just assign this as internal.dom here
- let hydrateDom =
- internal.flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE)
- ? startDom
- : null;
+ options._catchError(e, internal);
+ }
+ }
+
+ // internal.flags &= RESET_MODE;
+
+ if (options.diffed) options.diffed(internal);
- nextDomSibling = mountElement(internal, hydrateDom);
+ // If we just mounted a root node/Portal, and it changed the parentDom
+ // of it's children, then we need to resume the diff from it's previous
+ // startDom element, which could be null if we are mounting an entirely
+ // new tree, or the portal's nextSibling if we are mounting a Portal in
+ // an existing tree.
+ return prevStartDom || nextDomSibling;
+}
+
+/**
+ * @param {import('../internal').Internal} internal
+ * @param {any} props
+ * @param {import('../internal').PreactElement} parentDom
+ * @param {import('../internal').PreactNode} startDom
+ * @param {import('../internal').Internal['flags']} flags
+ */
+function mountComponent(internal, props, parentDom, startDom, flags) {
+ let type = /** @type {import('../internal').ComponentType} */ (internal.type);
+
+ let context = getParentContext(internal);
+
+ // Necessary for createContext api. Setting this property will pass
+ // the context value as `this.context` just for this component.
+ let tmp = type.contextType;
+ let provider = tmp && context[tmp._id];
+ let componentContext = tmp
+ ? provider
+ ? provider.props.value
+ : tmp._defaultValue
+ : context;
+ // inst.context = componentContext;
+
+ if (provider) provider._subs.add(internal);
+
+ let inst;
+ if (flags & TYPE_CLASS) {
+ // @ts-ignore `type` is a class component constructor
+ inst = new type(props, componentContext);
+ } else {
+ inst = {
+ props,
+ context: componentContext,
+ forceUpdate: internal.rerender.bind(null, internal)
+ };
+ }
+ inst._internal = internal;
+ internal._component = inst;
+ internal.flags |= DIRTY_BIT;
+
+ if (!inst.state) inst.state = {};
+ if (inst._nextState == null) inst._nextState = inst.state;
+
+ if (type.getDerivedStateFromProps != null) {
+ if (inst._nextState == inst.state) {
+ inst._nextState = Object.assign({}, inst._nextState);
}
- if (options.diffed) options.diffed(internal);
+ Object.assign(
+ inst._nextState,
+ type.getDerivedStateFromProps(props, inst._nextState)
+ );
+ } else if (inst.componentWillMount != null) {
+ inst.componentWillMount();
+ }
- // We successfully rendered this VNode, unset any stored hydration/bailout state:
- internal.flags &= RESET_MODE;
- } catch (e) {
- internal._vnodeId = 0;
- internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
-
- if (internal.flags & MODE_HYDRATE) {
- // @ts-ignore Trust me TS, nextSibling is a PreactElement
- nextDomSibling = startDom && startDom.nextSibling;
- internal._dom = startDom; // Save our current DOM position to resume later
+ // Enqueue componentDidMount to run the first time this internal commits
+ if (inst.componentDidMount != null) {
+ internal._commitCallbacks.push(inst.componentDidMount.bind(inst));
+ }
+
+ inst.context = componentContext;
+ inst.props = props;
+ inst.state = inst._nextState;
+
+ let renderHook = options._render;
+ let renderResult;
+
+ let counter = 0;
+ while (counter++ < 25) {
+ // mark as clean:
+ internal.flags &= ~DIRTY_BIT;
+ if (renderHook) renderHook(internal);
+ if (flags & TYPE_CLASS) {
+ renderResult = inst.render(inst.props, inst.state, inst.context);
+ // note: disable repeat render invocation for class components
+ break;
+ } else {
+ renderResult = type.call(inst, inst.props, inst.context);
+ }
+ // re-render if marked as dirty:
+ if (!(internal.flags & DIRTY_BIT)) {
+ break;
}
- options._catchError(e, internal);
}
+ // internal.flags &= ~DIRTY_BIT;
- return nextDomSibling;
+ // Handle setState called in render, see #2553
+ inst.state = inst._nextState;
+
+ if (inst.getChildContext != null) {
+ internal._context = Object.assign({}, context, inst.getChildContext());
+ }
+
+ if (renderResult == null) {
+ return startDom;
+ }
+
+ if (typeof renderResult === 'object') {
+ if (renderResult.type === Fragment && renderResult.key == null) {
+ renderResult = renderResult.props.children;
+ }
+ if (!Array.isArray(renderResult)) {
+ renderResult = [renderResult];
+ }
+ } else {
+ renderResult = [renderResult];
+ }
+
+ return mountChildren(internal, renderResult, parentDom, startDom);
}
/**
* Construct (or select, if hydrating) a new DOM element for the given Internal.
* @param {import('../internal').Internal} internal
- * @param {import('../internal').PreactNode} dom A DOM node to attempt to re-use during hydration
+ * @param {import('../internal').PreactElement} dom A DOM node to attempt to re-use during hydration
* @returns {import('../internal').PreactNode}
*/
function mountElement(internal, dom) {
- let newProps = internal.props;
let nodeType = internal.type;
let flags = internal.flags;
+ let newProps = internal.props;
// Are we rendering within an inline SVG?
let isSvg = flags & MODE_SVG;
@@ -162,124 +244,114 @@ function mountElement(internal, dom) {
// Are we *not* hydrating? (a top-level render() or mutative hydration):
let isFullRender = ~flags & MODE_HYDRATE;
- /** @type {any} */
- let i, value;
+ let hydrateChild = null;
+ let nextDomSibling;
- // if hydrating (hydrate() or render() with replaceNode), find the matching child:
+ // If hydrating (hydrate() or render() with replaceNode), find the matching child:
+ // Note: this flag guard is redundant, since `dom` is only non-null when hydrating.
+ // It has been left here purely for filesize reasons, as it saves 5b.
if (flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE)) {
- while (
- dom &&
- (nodeType ? dom.localName !== nodeType : dom.nodeType !== 3)
- ) {
- dom = dom.nextSibling;
+ while (dom) {
+ if (dom.localName === nodeType) {
+ hydrateChild = dom.firstChild;
+ nextDomSibling = dom.nextSibling;
+
+ if (flags & MODE_MUTATIVE_HYDRATE) {
+ // "Mutative Hydration":
+ // When hydrating an existing DOM tree within a full render, we diff attributes.
+ // This happens when a `replaceNode` value is passed to render().
+ //
+ // @TODO: Consider removing and recommending setting changed props after initial hydration.
+ // During normal hydration, no props are diffed - only event handlers are applied.
+ for (let i = 0; i < dom.attributes.length; i++) {
+ let value = dom.attributes[i].name;
+ if (!(value in newProps)) {
+ dom.removeAttribute(value);
+ }
+ }
+ }
+ break;
+ }
+ dom = dom.nextElementSibling;
}
}
- let isNew = dom == null;
-
- if (flags & TYPE_TEXT) {
- if (isNew) {
- // @ts-ignore createTextNode returns Text, we expect PreactElement
- dom = document.createTextNode(newProps);
- } else if (dom.data !== newProps) {
- dom.data = newProps;
- }
-
- internal._dom = dom;
- } else {
- // Tracks entering and exiting SVG namespace when descending through the tree.
- // if (nodeType === 'svg') internal.flags |= MODE_SVG;
-
- if (isNew) {
- if (isSvg) {
- dom = document.createElementNS(
- 'http://www.w3.org/2000/svg',
- // @ts-ignore We know `newVNode.type` is a string
- nodeType
- );
- } else {
- dom = document.createElement(
- // @ts-ignore We know `newVNode.type` is a string
- nodeType,
- newProps.is && newProps
- );
- }
-
- // we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate
- internal.flags = flags &= RESET_MODE;
- isFullRender = 1;
+ if (dom == null) {
+ if (isSvg) {
+ dom = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ // @ts-ignore We know `newVNode.type` is a string
+ nodeType
+ );
+ } else {
+ dom = document.createElement(
+ // @ts-ignore We know `newVNode.type` is a string
+ nodeType,
+ newProps.is && newProps
+ );
}
- // @TODO: Consider removing and instructing users to instead set the desired
- // prop for removal to undefined/null. During hydration, props are not
- // diffed at all (including dangerouslySetInnerHTML)
- if (flags & MODE_MUTATIVE_HYDRATE) {
- // But, if we are in a situation where we are using existing DOM (e.g. replaceNode)
- // we should read the existing DOM attributes to diff them
- for (i = 0; i < dom.attributes.length; i++) {
- value = dom.attributes[i].name;
- if (!(value in newProps)) {
- dom.removeAttribute(value);
- }
- }
- }
+ // We're creating a new node, which means its subtree is also new.
+ // If we were hydrating, this "deopts" the subtree into normal rendering mode.
+ internal.flags = flags &= RESET_MODE;
+ isFullRender = 1;
+ }
- let newHtml, newValue, newChildren;
- if (
- (nodeType === 'input' ||
- nodeType === 'textarea' ||
- nodeType === 'select') &&
- (newProps.onInput || newProps.onChange)
- ) {
- if (newProps.value != null) {
- dom._isControlled = true;
- dom._prevValue = newProps.value;
- } else if (newProps.checked != null) {
- dom._isControlled = true;
- dom._prevValue = newProps.checked;
- }
+ internal._dom = dom;
+
+ // Apply props
+ let newHtml, newValue, newChildren;
+ for (let i in newProps) {
+ let value = newProps[i];
+ if (i === 'children') {
+ newChildren = value;
+ } else if (i === 'dangerouslySetInnerHTML') {
+ newHtml = value;
+ } else if (i === 'value') {
+ newValue = value;
+ } else if (value != null && (isFullRender || typeof value === 'function')) {
+ setProperty(dom, i, value, null, isSvg);
}
+ }
- for (i in newProps) {
- value = newProps[i];
- if (i === 'children') {
- newChildren = value;
- } else if (i === 'dangerouslySetInnerHTML') {
- newHtml = value;
- } else if (i === 'value') {
- newValue = value;
- } else if (
- value != null &&
- (isFullRender || typeof value === 'function')
- ) {
- setProperty(dom, i, value, null, isSvg);
- }
+ // Install controlled input markers
+ if (
+ (nodeType === 'input' ||
+ nodeType === 'textarea' ||
+ nodeType === 'select') &&
+ (newProps.onInput || newProps.onChange)
+ ) {
+ if (newValue != null) {
+ dom._isControlled = true;
+ dom._prevValue = newValue;
+ } else if (newProps.checked != null) {
+ dom._isControlled = true;
+ dom._prevValue = newProps.checked;
}
+ }
- internal._dom = dom;
-
- // If the new vnode didn't have dangerouslySetInnerHTML, diff its children
- if (newHtml) {
- if (isFullRender && newHtml.__html) {
- dom.innerHTML = newHtml.__html;
- }
- } else if (newChildren != null) {
- mountChildren(
- internal,
- Array.isArray(newChildren) ? newChildren : [newChildren],
- dom,
- isNew ? null : dom.firstChild
- );
+ // If the new vnode didn't have dangerouslySetInnerHTML, diff its children
+ if (newHtml) {
+ if (isFullRender && newHtml.__html) {
+ dom.innerHTML = newHtml.__html;
}
+ } else if (newChildren != null) {
+ mountChildren(
+ internal,
+ Array.isArray(newChildren) ? newChildren : [newChildren],
+ dom,
+ hydrateChild // isNew ? null : dom.firstChild
+ );
+ }
- // (as above, don't diff props during hydration)
- if (isFullRender && newValue != null) {
- setProperty(dom, 'value', newValue, null, 0);
- }
+ // (as above, don't diff props during hydration)
+ if (isFullRender && newValue != null) {
+ setProperty(dom, 'value', newValue, null, 0);
}
// @ts-ignore
- return isNew ? null : dom.nextSibling;
+ return nextDomSibling;
+ // return isNew ? null : dom.nextSibling;
}
/**
diff --git a/src/diff/patch.js b/src/diff/patch.js
index a2922fb39e..476fbab279 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -1,7 +1,6 @@
import { patchChildren, insertComponentDom } from './children';
import { setProperty } from './props';
import options from '../options';
-import { renderClassComponent, renderFunctionComponent } from './component';
import {
RESET_MODE,
TYPE_TEXT,
@@ -15,8 +14,8 @@ import {
MODE_HYDRATE,
MODE_PENDING_ERROR,
MODE_RERENDERING_ERROR,
- SKIP_CHILDREN,
- DIRTY_BIT
+ DIRTY_BIT,
+ FORCE_UPDATE
} from '../constants';
import { getDomSibling, getParentContext } from '../tree';
import { mountChildren } from './mount';
@@ -30,150 +29,238 @@ import { commitQueue } from './renderer';
* @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered
*/
export function patch(internal, vnode, parentDom) {
+ if (options._diff) options._diff(internal, vnode);
+
let flags = internal.flags;
+ let prevProps = internal.props;
if (flags & TYPE_TEXT) {
- if (vnode !== internal.props) {
+ if (prevProps !== vnode) {
+ internal.props = vnode;
// @ts-ignore We know that newVNode is string/number/bigint, and internal._dom is Text
internal._dom.data = vnode;
- internal.props = vnode;
}
-
- return;
}
-
// When passing through createElement it assigns the object
// constructor as undefined. This to prevent JSON-injection.
- if (vnode.constructor !== UNDEFINED) return;
-
- if (options._diff) options._diff(internal, vnode);
+ else if (vnode.constructor === UNDEFINED) {
+ let newProps = vnode.props;
+ internal.props = newProps;
- // Root nodes render their children into a specific parent DOM element.
- // They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
- // @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
- let prevParentDom = parentDom;
- if (flags & TYPE_ROOT) {
- parentDom = vnode.props._parentDom;
+ // Root nodes render their children into a specific parent DOM element.
+ // They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
+ // @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
+ let prevParentDom = parentDom;
+ if (flags & TYPE_ROOT) {
+ parentDom = newProps._parentDom;
- if (internal.props._parentDom !== vnode.props._parentDom) {
- let nextSibling =
- parentDom == prevParentDom ? getDomSibling(internal) : null;
- insertComponentDom(internal, nextSibling, parentDom);
+ if (parentDom !== prevProps._parentDom) {
+ let nextSibling =
+ parentDom == prevParentDom ? getDomSibling(internal) : null;
+ insertComponentDom(internal, nextSibling, parentDom);
+ }
}
- }
- if (flags & TYPE_ELEMENT) {
- if (vnode._vnodeId !== internal._vnodeId) {
- // @ts-ignore dom is a PreactElement here
- patchElement(internal, vnode);
+ // Switch from MODE_PENDING_ERROR to MODE_RERENDERING_ERROR:
+ if (flags & MODE_PENDING_ERROR) {
+ flags = internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
}
- } else {
- try {
- if (internal.flags & MODE_PENDING_ERROR) {
- // Toggle the MODE_PENDING_ERROR and MODE_RERENDERING_ERROR flags. In
- // actuality, this should turn off the MODE_PENDING_ERROR flag and turn on
- // the MODE_RERENDERING_ERROR flag.
- internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
- }
-
- let context = getParentContext(internal);
-
- // Necessary for createContext api. Setting this property will pass
- // the context value as `this.context` just for this component.
- let tmp = vnode.type.contextType;
- let provider = tmp && context[tmp._id];
- let componentContext = tmp
- ? provider
- ? provider.props.value
- : tmp._defaultValue
- : context;
- let isNew = !internal || !internal._component;
-
- let renderResult;
- if (internal.flags & TYPE_CLASS) {
- renderResult = renderClassComponent(
- internal,
- vnode,
- context,
- componentContext
- );
+ let isSameVNode = vnode._vnodeId === internal._vnodeId;
+ if (!isSameVNode || (flags & FORCE_UPDATE) !== 0) {
+ if (flags & TYPE_ELEMENT) {
+ patchElement(internal, prevProps, newProps, flags);
} else {
- renderResult = renderFunctionComponent(
+ patchComponent(
internal,
- vnode,
- context,
- componentContext
+ internal._component,
+ prevProps,
+ newProps,
+ parentDom,
+ flags
);
- }
- if (renderResult == null) {
- renderResult = [];
- } else if (typeof renderResult === 'object') {
- if (renderResult.type === Fragment && renderResult.key == null) {
- renderResult = renderResult.props.children;
- }
- if (!Array.isArray(renderResult)) {
- renderResult = [renderResult];
+ if (internal._commitCallbacks.length) {
+ commitQueue.push(internal);
}
- } else {
- renderResult = [renderResult];
}
- if (internal.flags & SKIP_CHILDREN) {
- internal.props = vnode.props;
- internal.flags &= ~SKIP_CHILDREN;
- // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
- if (vnode && vnode._vnodeId !== internal._vnodeId) {
- internal.flags &= ~DIRTY_BIT;
- }
- } else if (internal._children == null) {
- let siblingDom =
- (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
- (MODE_HYDRATE | MODE_SUSPENDED)
- ? internal._dom
- : isNew || internal.flags & MODE_HYDRATE
- ? null
- : getDomSibling(internal);
-
- mountChildren(internal, renderResult, parentDom, siblingDom);
- } else {
- patchChildren(internal, renderResult, parentDom);
- }
+ // Once we have successfully rendered the new VNode, copy it's ID over
+ internal._vnodeId = vnode._vnodeId;
- if (internal._commitCallbacks.length) {
- commitQueue.push(internal);
- }
- } catch (e) {
- // @TODO: assign a new VNode ID here? Or NaN?
- // newVNode._vnodeId = 0;
- internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
- options._catchError(e, internal);
+ internal._prevRef = internal.ref;
+ internal.ref = vnode.ref;
}
}
- if (options.diffed) options.diffed(internal);
-
// We successfully rendered this VNode, unset any stored hydration/bailout state:
internal.flags &= RESET_MODE;
- // Once we have successfully rendered the new VNode, copy it's ID over
- internal._vnodeId = vnode._vnodeId;
+ if (options.diffed) options.diffed(internal);
+}
+
+/**
+ * @param {import('../internal').Internal} internal
+ * @param {import('../internal').Component} inst
+ * @param {any} prevProps
+ * @param {any} newProps
+ * @param {import('../internal').PreactElement} parentDom
+ * @param {import('../internal').Internal['flags']} flags
+ */
+function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
+ let type = /** @type {import('../internal').ComponentType} */ (internal.type);
+
+ let context = getParentContext(internal);
+
+ let snapshot;
+
+ let prevState = inst.state;
+ if (inst._nextState == null) {
+ inst._nextState = prevState;
+ }
+
+ // Necessary for createContext api. Setting this property will pass
+ // the context value as `this.context` just for this component.
+ let tmp = type.contextType;
+ let provider = tmp && context[tmp._id];
+ let componentContext = tmp
+ ? provider
+ ? provider.props.value
+ : tmp._defaultValue
+ : context;
+ // inst.context = componentContext;
+
+ try {
+ if (type.getDerivedStateFromProps != null) {
+ if (inst._nextState === prevState) {
+ inst._nextState = Object.assign({}, inst._nextState);
+ }
+
+ Object.assign(
+ inst._nextState,
+ type.getDerivedStateFromProps(newProps, inst._nextState)
+ );
+ }
+
+ if (
+ type.getDerivedStateFromProps == null &&
+ newProps !== prevProps &&
+ inst.componentWillReceiveProps != null
+ ) {
+ inst.componentWillReceiveProps(newProps, componentContext);
+ }
+
+ if (
+ !(flags & FORCE_UPDATE) &&
+ inst.shouldComponentUpdate != null &&
+ inst.shouldComponentUpdate(
+ newProps,
+ inst._nextState,
+ componentContext
+ ) === false
+ ) {
+ inst.state = inst._nextState;
+ inst._nextState = null;
+ inst.props = newProps;
- internal._prevRef = internal.ref;
- internal.ref = vnode.ref;
+ // @TODO: should this really be flipped?
+ internal.flags &= ~DIRTY_BIT;
+ return;
+ }
+
+ if (inst.componentWillUpdate != null) {
+ inst.componentWillUpdate(newProps, inst._nextState, componentContext);
+ }
+
+ inst.context = componentContext;
+ inst.props = newProps;
+ inst.state = inst._nextState;
+
+ let renderHook = options._render;
+
+ let renderResult;
+
+ let counter = 0;
+ while (counter++ < 25) {
+ // mark as clean:
+ internal.flags &= ~DIRTY_BIT;
+ if (renderHook) renderHook(internal);
+ if (flags & TYPE_CLASS) {
+ renderResult = inst.render(inst.props, inst.state, inst.context);
+ // note: disable repeat render invocation for class components
+ break;
+ } else {
+ renderResult = type.call(inst, inst.props, inst.context);
+ }
+ // re-render if marked as dirty:
+ if (!(internal.flags & DIRTY_BIT)) {
+ break;
+ }
+ }
+
+ // Handle setState called in render, see #2553
+ inst.state = inst._nextState;
+ // inst._nextState = null;
+
+ if (inst.getChildContext != null) {
+ internal._context = Object.assign({}, context, inst.getChildContext());
+ }
+
+ if (inst.getSnapshotBeforeUpdate != null) {
+ snapshot = inst.getSnapshotBeforeUpdate(prevProps, prevState);
+ }
+
+ // Only schedule componentDidUpdate if the component successfully rendered
+ if (inst.componentDidUpdate != null) {
+ internal._commitCallbacks.push(() => {
+ inst.componentDidUpdate(prevProps, prevState, snapshot);
+ });
+ }
+
+ if (renderResult == null) {
+ renderResult = [];
+ } else if (typeof renderResult === 'object') {
+ if (renderResult.type === Fragment && renderResult.key == null) {
+ renderResult = renderResult.props.children;
+ }
+ if (!Array.isArray(renderResult)) {
+ renderResult = [renderResult];
+ }
+ } else {
+ renderResult = [renderResult];
+ }
+
+ if (internal._children == null) {
+ let siblingDom =
+ (flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
+ (MODE_HYDRATE | MODE_SUSPENDED)
+ ? internal._dom
+ : flags & MODE_HYDRATE
+ ? null
+ : getDomSibling(internal);
+
+ mountChildren(internal, renderResult, parentDom, siblingDom);
+ } else {
+ patchChildren(internal, renderResult, parentDom);
+ }
+ } catch (e) {
+ // @TODO: assign a new VNode ID here? Or NaN?
+ // newVNode._vnodeId = 0;
+ internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
+ options._catchError(e, internal);
+ }
}
/**
* Update an internal and its associated DOM element based on a new VNode
* @param {import('../internal').Internal} internal
- * @param {import('../internal').VNode} vnode A VNode with props to compare and apply
+ * @param {any} oldProps
+ * @param {any} newProps
+ * @param {import('../internal').Internal['flags']} flags
*/
-function patchElement(internal, vnode) {
+function patchElement(internal, oldProps, newProps, flags) {
let dom = /** @type {import('../internal').PreactElement} */ (internal._dom),
- oldProps = internal.props,
- newProps = (internal.props = vnode.props),
- isSvg = internal.flags & MODE_SVG,
+ isSvg = flags & MODE_SVG,
i,
value,
tmp,
From 4770d40edbf73b73579589bd28b26601c8909d61 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Thu, 21 Apr 2022 17:54:56 -0400
Subject: [PATCH 04/14] Commented-out (but working) version of the original
patch() implementation that hoists equality check out of component rendering.
---
src/diff/component.js | 16 ++---
src/diff/patch.js | 150 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 153 insertions(+), 13 deletions(-)
diff --git a/src/diff/component.js b/src/diff/component.js
index e6d226c48c..04db0cd7fa 100644
--- a/src/diff/component.js
+++ b/src/diff/component.js
@@ -33,12 +33,6 @@ export function renderFunctionComponent(
internal.flags |= DIRTY_BIT;
}
- if (newVNode && newVNode._vnodeId === internal._vnodeId) {
- c.props = newProps;
- internal.flags |= SKIP_CHILDREN;
- return;
- }
-
c.context = componentContext;
internal.props = c.props = newProps;
@@ -132,13 +126,11 @@ export function renderClassComponent(
}
if (
- (!(internal.flags & FORCE_UPDATE) &&
- c.shouldComponentUpdate != null &&
- c.shouldComponentUpdate(newProps, c._nextState, componentContext) ===
- false) ||
- (newVNode && newVNode._vnodeId === internal._vnodeId)
+ !(internal.flags & FORCE_UPDATE) &&
+ c.shouldComponentUpdate != null &&
+ c.shouldComponentUpdate(newProps, c._nextState, componentContext) ===
+ false
) {
- c.props = newProps;
c.state = c._nextState;
internal.flags |= SKIP_CHILDREN;
return;
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 476fbab279..4faec6718a 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -230,12 +230,14 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
renderResult = [renderResult];
}
+ // patchChildren(internal, renderResult, parentDom);
+
if (internal._children == null) {
let siblingDom =
(flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
(MODE_HYDRATE | MODE_SUSPENDED)
? internal._dom
- : flags & MODE_HYDRATE
+ : flags & MODE_HYDRATE // : isNew || internal.flags & MODE_HYDRATE
? null
: getDomSibling(internal);
@@ -251,6 +253,150 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
}
}
+/*
+export function patch(internal, vnode, parentDom) {
+ let flags = internal.flags;
+
+ if (flags & TYPE_TEXT) {
+ if (vnode !== internal.props) {
+ // @ts-ignore We know that newVNode is string/number/bigint, and internal._dom is Text
+ internal._dom.data = vnode;
+ internal.props = vnode;
+ }
+
+ return;
+ }
+
+ // When passing through createElement it assigns the object
+ // constructor as undefined. This to prevent JSON-injection.
+ if (vnode.constructor !== UNDEFINED) return;
+
+ if (options._diff) options._diff(internal, vnode);
+
+ // Root nodes render their children into a specific parent DOM element.
+ // They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
+ // @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
+ let prevParentDom = parentDom;
+ if (flags & TYPE_ROOT) {
+ parentDom = vnode.props._parentDom;
+
+ if (internal.props._parentDom !== vnode.props._parentDom) {
+ let nextSibling =
+ parentDom == prevParentDom ? getDomSibling(internal) : null;
+ insertComponentDom(internal, nextSibling, parentDom);
+ }
+ }
+
+ if (flags & TYPE_ELEMENT) {
+ if (vnode._vnodeId !== internal._vnodeId) {
+ // @ts-ignore dom is a PreactElement here
+ patchElement(internal, internal.props, vnode.props, flags);
+ internal.props = vnode.props;
+ }
+ } else {
+ try {
+ if (internal.flags & MODE_PENDING_ERROR) {
+ // Toggle the MODE_PENDING_ERROR and MODE_RERENDERING_ERROR flags. In
+ // actuality, this should turn off the MODE_PENDING_ERROR flag and turn on
+ // the MODE_RERENDERING_ERROR flag.
+ internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
+ }
+
+ let renderResult;
+
+ if (vnode._vnodeId === internal._vnodeId) {
+ // internal._component.props = vnode.props;
+ internal.flags |= SKIP_CHILDREN;
+ } else {
+ let context = getParentContext(internal);
+
+ // Necessary for createContext api. Setting this property will pass
+ // the context value as `this.context` just for this component.
+ let tmp = vnode.type.contextType;
+ let provider = tmp && context[tmp._id];
+ let componentContext = tmp
+ ? provider
+ ? provider.props.value
+ : tmp._defaultValue
+ : context;
+
+ if (internal.flags & TYPE_CLASS) {
+ renderResult = renderClassComponent(
+ internal,
+ vnode,
+ context,
+ componentContext
+ );
+ } else {
+ renderResult = renderFunctionComponent(
+ internal,
+ vnode,
+ context,
+ componentContext
+ );
+ }
+
+ if (renderResult == null) {
+ renderResult = [];
+ } else if (typeof renderResult === 'object') {
+ if (renderResult.type === Fragment && renderResult.key == null) {
+ renderResult = renderResult.props.children;
+ }
+ if (!Array.isArray(renderResult)) {
+ renderResult = [renderResult];
+ }
+ } else {
+ renderResult = [renderResult];
+ }
+ }
+
+ if (internal.flags & SKIP_CHILDREN) {
+ internal.props = vnode.props;
+ internal._component.props = vnode.props;
+ internal.flags &= ~SKIP_CHILDREN;
+ // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
+ if (vnode && vnode._vnodeId !== internal._vnodeId) {
+ internal.flags &= ~DIRTY_BIT;
+ }
+ } else if (internal._children == null) {
+ let siblingDom =
+ (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
+ (MODE_HYDRATE | MODE_SUSPENDED)
+ ? internal._dom
+ : // : isNew || internal.flags & MODE_HYDRATE
+ internal.flags & MODE_HYDRATE
+ ? null
+ : getDomSibling(internal);
+
+ mountChildren(internal, renderResult, parentDom, siblingDom);
+ } else {
+ patchChildren(internal, renderResult, parentDom);
+ }
+
+ if (internal._commitCallbacks.length) {
+ commitQueue.push(internal);
+ }
+ } catch (e) {
+ // @TODO: assign a new VNode ID here? Or NaN?
+ // newVNode._vnodeId = 0;
+ internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
+ options._catchError(e, internal);
+ }
+ }
+
+ if (options.diffed) options.diffed(internal);
+
+ // We successfully rendered this VNode, unset any stored hydration/bailout state:
+ internal.flags &= RESET_MODE;
+
+ // Once we have successfully rendered the new VNode, copy it's ID over
+ internal._vnodeId = vnode._vnodeId;
+
+ internal._prevRef = internal.ref;
+ internal.ref = vnode.ref;
+}
+*/
+
/**
* Update an internal and its associated DOM element based on a new VNode
* @param {import('../internal').Internal} internal
@@ -260,6 +406,8 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
*/
function patchElement(internal, oldProps, newProps, flags) {
let dom = /** @type {import('../internal').PreactElement} */ (internal._dom),
+ // oldProps = internal.props,
+ // newProps = (internal.props = vnode.props),
isSvg = flags & MODE_SVG,
i,
value,
From 0c320579935f2bb7cd7624238c3b9acbfc2db01f Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Mon, 25 Apr 2022 10:01:36 -0400
Subject: [PATCH 05/14] perf/size tweaks
---
src/diff/mount.js | 26 +++++++++++++-------------
src/diff/patch.js | 31 ++++++++++++++-----------------
2 files changed, 27 insertions(+), 30 deletions(-)
diff --git a/src/diff/mount.js b/src/diff/mount.js
index 829e4a5500..680758e1e3 100644
--- a/src/diff/mount.js
+++ b/src/diff/mount.js
@@ -183,24 +183,24 @@ function mountComponent(internal, props, parentDom, startDom, flags) {
let renderHook = options._render;
let renderResult;
- let counter = 0;
- while (counter++ < 25) {
- // mark as clean:
+ // note: disable repeat render invocation for class components
+ if (flags & TYPE_CLASS) {
internal.flags &= ~DIRTY_BIT;
if (renderHook) renderHook(internal);
- if (flags & TYPE_CLASS) {
- renderResult = inst.render(inst.props, inst.state, inst.context);
- // note: disable repeat render invocation for class components
- break;
- } else {
+ renderResult = inst.render(inst.props, inst.state, inst.context);
+ } else {
+ let counter = 0;
+ while (counter++ < 25) {
+ // mark as clean:
+ internal.flags &= ~DIRTY_BIT;
+ if (renderHook) renderHook(internal);
renderResult = type.call(inst, inst.props, inst.context);
- }
- // re-render if marked as dirty:
- if (!(internal.flags & DIRTY_BIT)) {
- break;
+ // re-render if marked as dirty:
+ if (!(internal.flags & DIRTY_BIT)) {
+ break;
+ }
}
}
- // internal.flags &= ~DIRTY_BIT;
// Handle setState called in render, see #2553
inst.state = inst._nextState;
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 4faec6718a..94d024427b 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -140,10 +140,7 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
inst._nextState,
type.getDerivedStateFromProps(newProps, inst._nextState)
);
- }
-
- if (
- type.getDerivedStateFromProps == null &&
+ } else if (
newProps !== prevProps &&
inst.componentWillReceiveProps != null
) {
@@ -177,24 +174,24 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
inst.state = inst._nextState;
let renderHook = options._render;
-
let renderResult;
- let counter = 0;
- while (counter++ < 25) {
- // mark as clean:
+ // note: disable repeat render invocation for class components
+ if (flags & TYPE_CLASS) {
internal.flags &= ~DIRTY_BIT;
if (renderHook) renderHook(internal);
- if (flags & TYPE_CLASS) {
- renderResult = inst.render(inst.props, inst.state, inst.context);
- // note: disable repeat render invocation for class components
- break;
- } else {
+ renderResult = inst.render(inst.props, inst.state, inst.context);
+ } else {
+ let counter = 0;
+ while (counter++ < 25) {
+ // mark as clean:
+ internal.flags &= ~DIRTY_BIT;
+ if (renderHook) renderHook(internal);
renderResult = type.call(inst, inst.props, inst.context);
- }
- // re-render if marked as dirty:
- if (!(internal.flags & DIRTY_BIT)) {
- break;
+ // re-render if marked as dirty:
+ if (!(internal.flags & DIRTY_BIT)) {
+ break;
+ }
}
}
From 3d862dedceded23388e1cbb5ee50248699ec3f68 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Mon, 25 Apr 2022 12:38:46 -0400
Subject: [PATCH 06/14] simplify siblingDom calc and reduce size
---
src/diff/patch.js | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 94d024427b..075986fe79 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -227,16 +227,13 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
renderResult = [renderResult];
}
- // patchChildren(internal, renderResult, parentDom);
-
if (internal._children == null) {
- let siblingDom =
- (flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
- (MODE_HYDRATE | MODE_SUSPENDED)
- ? internal._dom
- : flags & MODE_HYDRATE // : isNew || internal.flags & MODE_HYDRATE
- ? null
- : getDomSibling(internal);
+ let siblingDom;
+ if (flags & MODE_HYDRATE) {
+ siblingDom = flags & MODE_SUSPENDED ? internal._dom : null;
+ } else {
+ siblingDom = getDomSibling(internal);
+ }
mountChildren(internal, renderResult, parentDom, siblingDom);
} else {
From 768f660013e941997d173d7303ccc2f28fa08941 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Mon, 25 Apr 2022 12:44:36 -0400
Subject: [PATCH 07/14] remove unused code
---
src/diff/component.js | 176 ------------------------------------------
src/diff/patch.js | 144 ----------------------------------
2 files changed, 320 deletions(-)
delete mode 100644 src/diff/component.js
diff --git a/src/diff/component.js b/src/diff/component.js
deleted file mode 100644
index 04db0cd7fa..0000000000
--- a/src/diff/component.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import options from '../options';
-import { DIRTY_BIT, FORCE_UPDATE, SKIP_CHILDREN } from '../constants';
-
-/**
- * Render a function component
- * @param {import('../internal').Internal} internal The component's backing Internal node
- * @param {import('../internal').VNode} newVNode The new virtual node
- * @param {any} context Full context object from the nearest ancestor component Internal
- * @param {any} componentContext Scoped/selected context for this component
- * @returns {import('../internal').ComponentChildren} the component's children
- */
-export function renderFunctionComponent(
- internal,
- newVNode,
- context,
- componentContext
-) {
- /** @type {import('../internal').Component} */
- let c;
-
- let type = /** @type {import('../internal').ComponentType} */ (internal.type);
-
- // @TODO split update + mount?
- let newProps = newVNode ? newVNode.props : internal.props;
-
- if (!(c = internal._component)) {
- internal._component = c = {
- props: newProps,
- context: componentContext,
- forceUpdate: internal.rerender.bind(null, internal)
- };
- c._internal = internal;
- internal.flags |= DIRTY_BIT;
- }
-
- c.context = componentContext;
- internal.props = c.props = newProps;
-
- let renderResult;
- let renderHook = options._render;
- let counter = 0;
- while (counter++ < 25) {
- internal.flags &= ~DIRTY_BIT;
- if (renderHook) renderHook(internal);
- renderResult = type.call(c, c.props, componentContext);
- if (!(internal.flags & DIRTY_BIT)) {
- break;
- }
- }
- internal.flags &= ~DIRTY_BIT;
- if (c.getChildContext != null) {
- internal._context = Object.assign({}, context, c.getChildContext());
- }
-
- return renderResult;
-}
-
-/**
- * Render a class component
- * @param {import('../internal').Internal} internal The component's backing Internal node
- * @param {import('../internal').VNode} newVNode The new virtual node
- * @param {any} context Full context object from the nearest ancestor component Internal
- * @param {any} componentContext Scoped/selected context for this component
- * @returns {import('../internal').ComponentChildren} the component's children
- */
-export function renderClassComponent(
- internal,
- newVNode,
- context,
- componentContext
-) {
- /** @type {import('../internal').Component} */
- let c;
- let isNew, oldProps, oldState, snapshot;
-
- let type = /** @type {import('../internal').ComponentType} */ (internal.type);
-
- // @TODO split update + mount?
- let newProps = newVNode ? newVNode.props : internal.props;
-
- if (!(c = internal._component)) {
- // @ts-ignore The check above verifies that newType is suppose to be constructed
- internal._component = c = new type(newProps, componentContext); // eslint-disable-line new-cap
-
- if (!c.state) c.state = {};
- isNew = true;
- c._internal = internal;
- internal.flags |= DIRTY_BIT;
- }
-
- // Invoke getDerivedStateFromProps
- if (c._nextState == null) {
- c._nextState = c.state;
- }
- if (type.getDerivedStateFromProps != null) {
- if (c._nextState == c.state) {
- c._nextState = Object.assign({}, c._nextState);
- }
-
- Object.assign(
- c._nextState,
- type.getDerivedStateFromProps(newProps, c._nextState)
- );
- }
-
- oldProps = c.props;
- oldState = c.state;
- if (isNew) {
- if (type.getDerivedStateFromProps == null && c.componentWillMount != null) {
- c.componentWillMount();
- }
-
- if (c.componentDidMount != null) {
- // If the component was constructed, queue up componentDidMount so the
- // first time this internal commits (regardless of suspense or not) it
- // will be called
- internal._commitCallbacks.push(c.componentDidMount.bind(c));
- }
- } else {
- if (
- type.getDerivedStateFromProps == null &&
- newProps !== oldProps &&
- c.componentWillReceiveProps != null
- ) {
- c.componentWillReceiveProps(newProps, componentContext);
- }
-
- if (
- !(internal.flags & FORCE_UPDATE) &&
- c.shouldComponentUpdate != null &&
- c.shouldComponentUpdate(newProps, c._nextState, componentContext) ===
- false
- ) {
- c.state = c._nextState;
- internal.flags |= SKIP_CHILDREN;
- return;
- }
-
- if (c.componentWillUpdate != null) {
- c.componentWillUpdate(newProps, c._nextState, componentContext);
- }
- }
-
- c.context = componentContext;
- internal.props = c.props = newProps;
- c.state = c._nextState;
-
- let renderHook = options._render;
- if (renderHook) renderHook(internal);
-
- internal.flags &= ~DIRTY_BIT;
-
- let renderResult = c.render(c.props, c.state, c.context);
-
- // Handle setState called in render, see #2553
- c.state = c._nextState;
-
- if (c.getChildContext != null) {
- internal._context = Object.assign({}, context, c.getChildContext());
- }
-
- if (!isNew) {
- if (c.getSnapshotBeforeUpdate != null) {
- snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
- }
-
- // Only schedule componentDidUpdate if the component successfully rendered
- if (c.componentDidUpdate != null) {
- internal._commitCallbacks.push(() => {
- c.componentDidUpdate(oldProps, oldState, snapshot);
- });
- }
- }
-
- return renderResult;
-}
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 075986fe79..1c22f0c86e 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -247,150 +247,6 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
}
}
-/*
-export function patch(internal, vnode, parentDom) {
- let flags = internal.flags;
-
- if (flags & TYPE_TEXT) {
- if (vnode !== internal.props) {
- // @ts-ignore We know that newVNode is string/number/bigint, and internal._dom is Text
- internal._dom.data = vnode;
- internal.props = vnode;
- }
-
- return;
- }
-
- // When passing through createElement it assigns the object
- // constructor as undefined. This to prevent JSON-injection.
- if (vnode.constructor !== UNDEFINED) return;
-
- if (options._diff) options._diff(internal, vnode);
-
- // Root nodes render their children into a specific parent DOM element.
- // They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
- // @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
- let prevParentDom = parentDom;
- if (flags & TYPE_ROOT) {
- parentDom = vnode.props._parentDom;
-
- if (internal.props._parentDom !== vnode.props._parentDom) {
- let nextSibling =
- parentDom == prevParentDom ? getDomSibling(internal) : null;
- insertComponentDom(internal, nextSibling, parentDom);
- }
- }
-
- if (flags & TYPE_ELEMENT) {
- if (vnode._vnodeId !== internal._vnodeId) {
- // @ts-ignore dom is a PreactElement here
- patchElement(internal, internal.props, vnode.props, flags);
- internal.props = vnode.props;
- }
- } else {
- try {
- if (internal.flags & MODE_PENDING_ERROR) {
- // Toggle the MODE_PENDING_ERROR and MODE_RERENDERING_ERROR flags. In
- // actuality, this should turn off the MODE_PENDING_ERROR flag and turn on
- // the MODE_RERENDERING_ERROR flag.
- internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
- }
-
- let renderResult;
-
- if (vnode._vnodeId === internal._vnodeId) {
- // internal._component.props = vnode.props;
- internal.flags |= SKIP_CHILDREN;
- } else {
- let context = getParentContext(internal);
-
- // Necessary for createContext api. Setting this property will pass
- // the context value as `this.context` just for this component.
- let tmp = vnode.type.contextType;
- let provider = tmp && context[tmp._id];
- let componentContext = tmp
- ? provider
- ? provider.props.value
- : tmp._defaultValue
- : context;
-
- if (internal.flags & TYPE_CLASS) {
- renderResult = renderClassComponent(
- internal,
- vnode,
- context,
- componentContext
- );
- } else {
- renderResult = renderFunctionComponent(
- internal,
- vnode,
- context,
- componentContext
- );
- }
-
- if (renderResult == null) {
- renderResult = [];
- } else if (typeof renderResult === 'object') {
- if (renderResult.type === Fragment && renderResult.key == null) {
- renderResult = renderResult.props.children;
- }
- if (!Array.isArray(renderResult)) {
- renderResult = [renderResult];
- }
- } else {
- renderResult = [renderResult];
- }
- }
-
- if (internal.flags & SKIP_CHILDREN) {
- internal.props = vnode.props;
- internal._component.props = vnode.props;
- internal.flags &= ~SKIP_CHILDREN;
- // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
- if (vnode && vnode._vnodeId !== internal._vnodeId) {
- internal.flags &= ~DIRTY_BIT;
- }
- } else if (internal._children == null) {
- let siblingDom =
- (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) ===
- (MODE_HYDRATE | MODE_SUSPENDED)
- ? internal._dom
- : // : isNew || internal.flags & MODE_HYDRATE
- internal.flags & MODE_HYDRATE
- ? null
- : getDomSibling(internal);
-
- mountChildren(internal, renderResult, parentDom, siblingDom);
- } else {
- patchChildren(internal, renderResult, parentDom);
- }
-
- if (internal._commitCallbacks.length) {
- commitQueue.push(internal);
- }
- } catch (e) {
- // @TODO: assign a new VNode ID here? Or NaN?
- // newVNode._vnodeId = 0;
- internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED;
- options._catchError(e, internal);
- }
- }
-
- if (options.diffed) options.diffed(internal);
-
- // We successfully rendered this VNode, unset any stored hydration/bailout state:
- internal.flags &= RESET_MODE;
-
- // Once we have successfully rendered the new VNode, copy it's ID over
- internal._vnodeId = vnode._vnodeId;
-
- internal._prevRef = internal.ref;
- internal.ref = vnode.ref;
-}
-*/
-
/**
* Update an internal and its associated DOM element based on a new VNode
* @param {import('../internal').Internal} internal
From 3bc6915b0194018e9a87ff226d3c8cb4bba34fd8 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Tue, 26 Apr 2022 17:08:50 -0400
Subject: [PATCH 08/14] Remove parentDom everywhere, replace with a conditional
lookup
---
src/create-root.js | 4 +--
src/diff/children.js | 36 ++++++++++++++----------
src/diff/mount.js | 46 ++++++++++++++-----------------
src/diff/patch.js | 25 ++++++++---------
src/tree.js | 34 ++++++++++++++---------
test/browser/getParentDom.test.js | 30 ++++++++++++--------
6 files changed, 97 insertions(+), 78 deletions(-)
diff --git a/src/create-root.js b/src/create-root.js
index 58f8c5f9a6..228b28bf40 100644
--- a/src/create-root.js
+++ b/src/create-root.js
@@ -30,7 +30,7 @@ export function createRoot(parentDom) {
/** @type {import('./internal').PreactElement} */ (parentDom.firstChild);
if (rootInternal) {
- patch(rootInternal, vnode, parentDom);
+ patch(rootInternal, vnode);
} else {
rootInternal = createInternal(vnode);
@@ -49,7 +49,7 @@ export function createRoot(parentDom) {
rootInternal._context = {};
- mount(rootInternal, vnode, parentDom, firstChild);
+ mount(rootInternal, vnode, firstChild);
}
// Flush all queued effects
diff --git a/src/diff/children.js b/src/diff/children.js
index 40d06ac852..da7d61d83f 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -11,21 +11,27 @@ import {
import { mount } from './mount';
import { patch } from './patch';
import { unmount } from './unmount';
-import { createInternal, getDomSibling } from '../tree';
+import { createInternal, getDomSibling, getParentDom } from '../tree';
/**
* Update an internal with new children.
* @param {import('../internal').Internal} internal The internal whose children should be patched
* @param {import('../internal').ComponentChild[]} children The new children, represented as VNodes
- * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered
*/
-export function patchChildren(internal, children, parentDom) {
+export function patchChildren(internal, children) {
let oldChildren =
(internal._children && internal._children.slice()) || EMPTY_ARR;
let oldChildrenLength = oldChildren.length;
let remainingOldChildren = oldChildrenLength;
+ /**
+ * This is the result of `getParentDom(internal)`,
+ * but lazily-computed only only on insertion.
+ * @type {import('../internal').PreactElement}
+ */
+ let parentDom;
+
let skew = 0;
let i;
@@ -72,12 +78,7 @@ export function patchChildren(internal, children, parentDom) {
childInternal = createInternal(childVNode, internal);
// We are mounting a new VNode
- mount(
- childInternal,
- childVNode,
- parentDom,
- getDomSibling(internal, skewedIndex)
- );
+ mount(childInternal, childVNode, getDomSibling(internal, skewedIndex));
}
// If this node suspended during hydration, and no other flags are set:
// @TODO: might be better to explicitly check for MODE_ERRORED here.
@@ -86,10 +87,10 @@ export function patchChildren(internal, children, parentDom) {
(MODE_HYDRATE | MODE_SUSPENDED)
) {
// We are resuming the hydration of a VNode
- mount(childInternal, childVNode, parentDom, childInternal._dom);
+ mount(childInternal, childVNode, childInternal._dom);
} else {
// Morph the old element into the new one, but don't append it to the dom yet
- patch(childInternal, childVNode, parentDom);
+ patch(childInternal, childVNode);
}
go: if (mountingChild) {
@@ -99,7 +100,7 @@ export function patchChildren(internal, children, parentDom) {
// Perform insert of new dom
if (childInternal.flags & TYPE_DOM) {
- parentDom.insertBefore(
+ (parentDom || (parentDom = getParentDom(internal))).insertBefore(
childInternal._dom,
getDomSibling(internal, skewedIndex)
);
@@ -133,9 +134,16 @@ export function patchChildren(internal, children, parentDom) {
let nextSibling = getDomSibling(internal, skewedIndex + 1);
if (childInternal.flags & TYPE_DOM) {
- parentDom.insertBefore(childInternal._dom, nextSibling);
+ (parentDom || (parentDom = getParentDom(internal))).insertBefore(
+ childInternal._dom,
+ nextSibling
+ );
} else {
- insertComponentDom(childInternal, nextSibling, parentDom);
+ insertComponentDom(
+ childInternal,
+ nextSibling,
+ parentDom || (parentDom = getParentDom(internal))
+ );
}
}
diff --git a/src/diff/mount.js b/src/diff/mount.js
index 680758e1e3..b26f45a829 100644
--- a/src/diff/mount.js
+++ b/src/diff/mount.js
@@ -16,18 +16,17 @@ import {
import options from '../options';
import { normalizeToVNode, Fragment } from '../create-element';
import { setProperty } from './props';
-import { createInternal, getParentContext } from '../tree';
+import { createInternal, getParentContext, getParentDom } from '../tree';
import { commitQueue } from './renderer';
/**
* Diff two virtual nodes and apply proper changes to the DOM
* @param {import('../internal').Internal} internal The Internal node to mount
* @param {import('../internal').VNode | string} vnode The new virtual node
- * @param {import('../internal').PreactElement} parentDom The element into which this subtree should be inserted
* @param {import('../internal').PreactNode} startDom
* @returns {import('../internal').PreactNode | null} pointer to the next DOM node to be hydrated (or null)
*/
-export function mount(internal, vnode, parentDom, startDom) {
+export function mount(internal, vnode, startDom) {
if (options._diff) options._diff(internal, vnode);
let flags = internal.flags;
@@ -44,11 +43,12 @@ export function mount(internal, vnode, parentDom, startDom) {
// the page. Root nodes can occur anywhere in the tree and not just at the
// top.
let prevStartDom;
- let prevParentDom = parentDom;
if (flags & TYPE_ROOT) {
- parentDom = props._parentDom;
+ // Normally, the DOM parent for insertions is the parent internal's DOM.
+ // However, the root Internal in a tree has no _parent, an inserts into props._parentDom.
+ let implicitParentDom = getParentDom(internal._parent || internal);
- if (parentDom !== prevParentDom) {
+ if (props._parentDom !== implicitParentDom) {
prevStartDom = startDom;
startDom = null;
}
@@ -76,13 +76,7 @@ export function mount(internal, vnode, parentDom, startDom) {
internal.flags &= RESET_MODE;
} else {
try {
- nextDomSibling = mountComponent(
- internal,
- props,
- parentDom,
- startDom,
- flags
- );
+ nextDomSibling = mountComponent(internal, props, startDom, flags);
if (internal._commitCallbacks.length) {
commitQueue.push(internal);
@@ -118,11 +112,10 @@ export function mount(internal, vnode, parentDom, startDom) {
/**
* @param {import('../internal').Internal} internal
* @param {any} props
- * @param {import('../internal').PreactElement} parentDom
* @param {import('../internal').PreactNode} startDom
* @param {import('../internal').Internal['flags']} flags
*/
-function mountComponent(internal, props, parentDom, startDom, flags) {
+function mountComponent(internal, props, startDom, flags) {
let type = /** @type {import('../internal').ComponentType} */ (internal.type);
let context = getParentContext(internal);
@@ -224,7 +217,7 @@ function mountComponent(internal, props, parentDom, startDom, flags) {
renderResult = [renderResult];
}
- return mountChildren(internal, renderResult, parentDom, startDom);
+ return mountChildren(internal, renderResult, startDom);
}
/**
@@ -244,7 +237,9 @@ function mountElement(internal, dom) {
// Are we *not* hydrating? (a top-level render() or mutative hydration):
let isFullRender = ~flags & MODE_HYDRATE;
+ /** @type {import('../internal').PreactNode} */
let hydrateChild = null;
+ /** @type {import('../internal').PreactNode} */
let nextDomSibling;
// If hydrating (hydrate() or render() with replaceNode), find the matching child:
@@ -253,7 +248,9 @@ function mountElement(internal, dom) {
if (flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE)) {
while (dom) {
if (dom.localName === nodeType) {
+ // @ts-ignore-next ChildNode ~= PreactNode
hydrateChild = dom.firstChild;
+ // @ts-ignore-next ChildNode ~= PreactNode
nextDomSibling = dom.nextSibling;
if (flags & MODE_MUTATIVE_HYDRATE) {
@@ -339,8 +336,7 @@ function mountElement(internal, dom) {
mountChildren(
internal,
Array.isArray(newChildren) ? newChildren : [newChildren],
- dom,
- hydrateChild // isNew ? null : dom.firstChild
+ hydrateChild
);
}
@@ -349,25 +345,23 @@ function mountElement(internal, dom) {
setProperty(dom, 'value', newValue, null, 0);
}
- // @ts-ignore
return nextDomSibling;
- // return isNew ? null : dom.nextSibling;
}
/**
* Mount all children of an Internal
* @param {import('../internal').Internal} internal The parent Internal of the given children
* @param {import('../internal').ComponentChild[]} children
- * @param {import('../internal').PreactElement} parentDom The element into which this subtree should be inserted
* @param {import('../internal').PreactNode} startDom
*/
-export function mountChildren(internal, children, parentDom, startDom) {
+export function mountChildren(internal, children, startDom) {
let internalChildren = (internal._children = []),
i,
childVNode,
childInternal,
newDom,
- mountedNextChild;
+ mountedNextChild,
+ parentDom;
for (i = 0; i < children.length; i++) {
childVNode = normalizeToVNode(children[i]);
@@ -383,7 +377,7 @@ export function mountChildren(internal, children, parentDom, startDom) {
internalChildren[i] = childInternal;
// Morph the old element into the new one, but don't append it to the dom yet
- mountedNextChild = mount(childInternal, childVNode, parentDom, startDom);
+ mountedNextChild = mount(childInternal, childVNode, startDom);
newDom = childInternal._dom;
@@ -393,10 +387,12 @@ export function mountChildren(internal, children, parentDom, startDom) {
// continue with the mountedNextChild
startDom = mountedNextChild;
} else if (newDom != null) {
+ let parent = parentDom || (parentDom = getParentDom(internal));
+
// The DOM the diff should begin with is now startDom (since we inserted
// newDom before startDom) so ignore mountedNextChild and continue with
// startDom
- parentDom.insertBefore(newDom, startDom);
+ parent.insertBefore(newDom, startDom);
}
if (childInternal.ref) {
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 1c22f0c86e..43fed6506e 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -17,7 +17,7 @@ import {
DIRTY_BIT,
FORCE_UPDATE
} from '../constants';
-import { getDomSibling, getParentContext } from '../tree';
+import { getDomSibling, getParentContext, getParentDom } from '../tree';
import { mountChildren } from './mount';
import { Fragment } from '../create-element';
import { commitQueue } from './renderer';
@@ -26,9 +26,8 @@ import { commitQueue } from './renderer';
* Diff two virtual nodes and apply proper changes to the DOM
* @param {import('../internal').Internal} internal The Internal node to patch
* @param {import('../internal').VNode | string} vnode The new virtual node
- * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered
*/
-export function patch(internal, vnode, parentDom) {
+export function patch(internal, vnode) {
if (options._diff) options._diff(internal, vnode);
let flags = internal.flags;
@@ -50,13 +49,16 @@ export function patch(internal, vnode, parentDom) {
// Root nodes render their children into a specific parent DOM element.
// They can occur anywhere in the tree, can be nested, and currently allow reparenting during patches.
// @TODO: Decide if we actually want to support silent reparenting during patch - is it worth the bytes?
- let prevParentDom = parentDom;
if (flags & TYPE_ROOT) {
- parentDom = newProps._parentDom;
+ let parentDom = newProps._parentDom;
if (parentDom !== prevProps._parentDom) {
+ // Suspended trees are re-parented into the same parent so they can be inserted/removed without diffing.
+ // For that to work, when createPortal is used to render into the nearest element parent, we insert in-order.
let nextSibling =
- parentDom == prevParentDom ? getDomSibling(internal) : null;
+ parentDom == getParentDom(internal._parent)
+ ? getDomSibling(internal)
+ : null;
insertComponentDom(internal, nextSibling, parentDom);
}
}
@@ -76,7 +78,6 @@ export function patch(internal, vnode, parentDom) {
internal._component,
prevProps,
newProps,
- parentDom,
flags
);
@@ -104,10 +105,9 @@ export function patch(internal, vnode, parentDom) {
* @param {import('../internal').Component} inst
* @param {any} prevProps
* @param {any} newProps
- * @param {import('../internal').PreactElement} parentDom
* @param {import('../internal').Internal['flags']} flags
*/
-function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
+function patchComponent(internal, inst, prevProps, newProps, flags) {
let type = /** @type {import('../internal').ComponentType} */ (internal.type);
let context = getParentContext(internal);
@@ -235,9 +235,9 @@ function patchComponent(internal, inst, prevProps, newProps, parentDom, flags) {
siblingDom = getDomSibling(internal);
}
- mountChildren(internal, renderResult, parentDom, siblingDom);
+ mountChildren(internal, renderResult, siblingDom);
} else {
- patchChildren(internal, renderResult, parentDom);
+ patchChildren(internal, renderResult);
}
} catch (e) {
// @TODO: assign a new VNode ID here? Or NaN?
@@ -302,8 +302,7 @@ function patchElement(internal, oldProps, newProps, flags) {
if (oldHtml) dom.innerHTML = '';
patchChildren(
internal,
- newChildren && Array.isArray(newChildren) ? newChildren : [newChildren],
- dom
+ newChildren && Array.isArray(newChildren) ? newChildren : [newChildren]
);
}
diff --git a/src/tree.js b/src/tree.js
index 82a3103e10..db30feb918 100644
--- a/src/tree.js
+++ b/src/tree.js
@@ -183,23 +183,31 @@ export function getParentContext(internal) {
}
/**
+ * Returns the nearest DOM element for a given internal.
+ * - Component internal: returns its nearest Element parent.
+ * - Root internal: returns the its `_parentDom` prop.
+ * - Element internal: returns its associated DOM element.
* @param {import('./internal').Internal} internal
* @returns {import('./internal').PreactElement}
*/
export function getParentDom(internal) {
- let parent = internal;
-
- // if this is a Root internal, return its parent DOM:
- if (parent.flags & TYPE_ROOT) {
- return parent.props._parentDom;
+ if (internal.flags & TYPE_ROOT) {
+ return internal.props._parentDom;
}
-
- // walk up the tree to find the nearest DOM or Root Internal:
- while ((parent = parent._parent)) {
- if (parent.flags & TYPE_ROOT) {
- return parent.props._parentDom;
- } else if (parent.flags & TYPE_ELEMENT) {
- return parent._dom;
- }
+ if (internal.flags & TYPE_ELEMENT) {
+ // @ts-ignore-next the flag guard ensures _dom is a PreactElement
+ return internal._dom;
}
+ return internal._parent && getParentDom(internal._parent);
+
+ // while (internal) {
+ // if (internal.flags & TYPE_ROOT) {
+ // return internal.props._parentDom;
+ // }
+ // if (internal.flags & TYPE_ELEMENT) {
+ // // @ts-ignore-next the flag guard ensures _dom is a PreactElement
+ // return internal._dom;
+ // }
+ // internal = internal._parent;
+ // }
}
diff --git a/test/browser/getParentDom.test.js b/test/browser/getParentDom.test.js
index 237816ee95..c6fcd3c36d 100644
--- a/test/browser/getParentDom.test.js
+++ b/test/browser/getParentDom.test.js
@@ -37,7 +37,7 @@ describe('getParentDom', () => {
let domInternals = getRoot(scratch)._children[0]._children;
for (let internal of domInternals) {
expect(internal.type).to.equal('div');
- expect(getParentDom(internal)).to.equalNode(scratch.firstChild);
+ expect(getParentDom(internal._parent)).to.equalNode(scratch.firstChild);
}
});
@@ -53,7 +53,9 @@ describe('getParentDom', () => {
let expectedTypes = ['div', null, 'div'];
for (let i = 0; i < domInternals.length; i++) {
expect(domInternals[i].type).to.equal(expectedTypes[i]);
- expect(getParentDom(domInternals[i])).to.equalNode(scratch.firstChild);
+ expect(getParentDom(domInternals[i]._parent)).to.equalNode(
+ scratch.firstChild
+ );
}
});
@@ -76,7 +78,9 @@ describe('getParentDom', () => {
let expectedTypes = ['div', null];
for (let i = 0; i < domInternals.length; i++) {
expect(domInternals[i].type).to.equal(expectedTypes[i]);
- expect(getParentDom(domInternals[i])).to.equalNode(scratch.firstChild);
+ expect(getParentDom(domInternals[i]._parent)).to.equalNode(
+ scratch.firstChild
+ );
}
});
@@ -104,7 +108,9 @@ describe('getParentDom', () => {
let expectedTypes = ['div', null];
for (let i = 0; i < domInternals.length; i++) {
expect(domInternals[i].type).to.equal(expectedTypes[i]);
- expect(getParentDom(domInternals[i])).to.equalNode(scratch.firstChild);
+ expect(getParentDom(domInternals[i]._parent)).to.equalNode(
+ scratch.firstChild
+ );
}
});
@@ -134,7 +140,9 @@ describe('getParentDom', () => {
let expectedTypes = [Foo, Fragment];
for (let i = 0; i < domInternals.length; i++) {
expect(domInternals[i].type).to.equal(expectedTypes[i]);
- expect(getParentDom(domInternals[i])).to.equalNode(scratch.firstChild);
+ expect(getParentDom(domInternals[i]._parent)).to.equalNode(
+ scratch.firstChild
+ );
}
});
@@ -152,7 +160,7 @@ describe('getParentDom', () => {
let internal = getRoot(scratch)._children[0]._children[1]._children[0]
._children[0];
- let parentDom = getParentDom(internal);
+ let parentDom = getParentDom(internal._parent);
expect(internal.type).to.equal('span');
expect(scratch.firstChild.childNodes[1].nodeName).to.equal('P');
@@ -177,7 +185,7 @@ describe('getParentDom', () => {
let internal = getRoot(scratch)._children[0]._children[0]._children[0]
._children[0];
- let parent = getParentDom(internal);
+ let parent = getParentDom(internal._parent);
expect(internal.type).to.equal('p');
expect(parent).to.equalNode(scratch.firstChild);
@@ -194,7 +202,7 @@ describe('getParentDom', () => {
const internal = getRoot(scratch)._children[0];
expect(internal.type).to.equal(Foo);
- expect(getParentDom(internal)).to.equal(scratch);
+ expect(getParentDom(internal._parent)).to.equal(scratch);
});
it('should return parentDom of root node', () => {
@@ -240,11 +248,11 @@ describe('getParentDom', () => {
let fooInternal = getRoot(scratch)._children[0]._children[0]._children[0];
expect(fooInternal.type).to.equal(Foo);
- expect(getParentDom(fooInternal)).to.equalNode(portalParent);
+ expect(getParentDom(fooInternal._parent)).to.equalNode(portalParent);
let divInternal = fooInternal._children[0];
expect(divInternal.type).to.equal('div');
- expect(getParentDom(divInternal)).to.equalNode(portalParent);
+ expect(getParentDom(divInternal._parent)).to.equalNode(portalParent);
});
it('should return parentDom of root node returned from a Component', () => {
@@ -263,6 +271,6 @@ describe('getParentDom', () => {
let internal = getRoot(scratch)._children[0]._children[0]._children[0]
._children[0];
expect(internal.type).to.equal('div');
- expect(getParentDom(internal)).to.equalNode(portalParent);
+ expect(getParentDom(internal._parent)).to.equalNode(portalParent);
});
});
From 52ff077ab2744736fd0b2fad3f089eeb23d69e02 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Wed, 27 Apr 2022 11:54:02 -0400
Subject: [PATCH 09/14] optimization: hoist _dom access out of patchElement
---
src/diff/patch.js | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 43fed6506e..53acbb4d6f 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -71,7 +71,14 @@ export function patch(internal, vnode) {
let isSameVNode = vnode._vnodeId === internal._vnodeId;
if (!isSameVNode || (flags & FORCE_UPDATE) !== 0) {
if (flags & TYPE_ELEMENT) {
- patchElement(internal, prevProps, newProps, flags);
+ patchElement(
+ internal,
+ // @ts-ignore _dom is a PreactElement here
+ internal._dom,
+ prevProps,
+ newProps,
+ flags
+ );
} else {
patchComponent(
internal,
@@ -250,15 +257,13 @@ function patchComponent(internal, inst, prevProps, newProps, flags) {
/**
* Update an internal and its associated DOM element based on a new VNode
* @param {import('../internal').Internal} internal
+ * @param {import('../internal').PreactElement} dom
* @param {any} oldProps
* @param {any} newProps
* @param {import('../internal').Internal['flags']} flags
*/
-function patchElement(internal, oldProps, newProps, flags) {
- let dom = /** @type {import('../internal').PreactElement} */ (internal._dom),
- // oldProps = internal.props,
- // newProps = (internal.props = vnode.props),
- isSvg = flags & MODE_SVG,
+function patchElement(internal, dom, oldProps, newProps, flags) {
+ let isSvg = flags & MODE_SVG,
i,
value,
tmp,
From 722863aa662e39fffbc6a11f44865243252c63c9 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Wed, 27 Apr 2022 14:29:59 -0400
Subject: [PATCH 10/14] cache options hook accesses (-10b)
---
src/diff/patch.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 53acbb4d6f..5e77241a8e 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -28,7 +28,8 @@ import { commitQueue } from './renderer';
* @param {import('../internal').VNode | string} vnode The new virtual node
*/
export function patch(internal, vnode) {
- if (options._diff) options._diff(internal, vnode);
+ let diffHook;
+ if ((diffHook = options._diff)) diffHook(internal, vnode);
let flags = internal.flags;
let prevProps = internal.props;
@@ -104,7 +105,7 @@ export function patch(internal, vnode) {
// We successfully rendered this VNode, unset any stored hydration/bailout state:
internal.flags &= RESET_MODE;
- if (options.diffed) options.diffed(internal);
+ if ((diffHook = options.diffed)) diffHook(internal);
}
/**
From 5add8d380784d4f0ba3bf7a1866492d4cf32fdf7 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Wed, 27 Apr 2022 15:22:20 -0400
Subject: [PATCH 11/14] revert diff hook caching
---
src/diff/patch.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 5e77241a8e..53acbb4d6f 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -28,8 +28,7 @@ import { commitQueue } from './renderer';
* @param {import('../internal').VNode | string} vnode The new virtual node
*/
export function patch(internal, vnode) {
- let diffHook;
- if ((diffHook = options._diff)) diffHook(internal, vnode);
+ if (options._diff) options._diff(internal, vnode);
let flags = internal.flags;
let prevProps = internal.props;
@@ -105,7 +104,7 @@ export function patch(internal, vnode) {
// We successfully rendered this VNode, unset any stored hydration/bailout state:
internal.flags &= RESET_MODE;
- if ((diffHook = options.diffed)) diffHook(internal);
+ if (options.diffed) options.diffed(internal);
}
/**
From 5e6b4cc81c24abfee08e156d9683eb1e6a2c456d Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Wed, 27 Apr 2022 20:16:19 -0400
Subject: [PATCH 12/14] (revert)
---
src/diff/patch.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 53acbb4d6f..5e77241a8e 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -28,7 +28,8 @@ import { commitQueue } from './renderer';
* @param {import('../internal').VNode | string} vnode The new virtual node
*/
export function patch(internal, vnode) {
- if (options._diff) options._diff(internal, vnode);
+ let diffHook;
+ if ((diffHook = options._diff)) diffHook(internal, vnode);
let flags = internal.flags;
let prevProps = internal.props;
@@ -104,7 +105,7 @@ export function patch(internal, vnode) {
// We successfully rendered this VNode, unset any stored hydration/bailout state:
internal.flags &= RESET_MODE;
- if (options.diffed) options.diffed(internal);
+ if ((diffHook = options.diffed)) diffHook(internal);
}
/**
From 7f631fd8ca03738389b8eea1951341143d99a2bb Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Thu, 28 Apr 2022 14:53:47 -0400
Subject: [PATCH 13/14] fix all type errors
---
src/component.js | 3 +--
src/diff/children.js | 9 +++++++--
src/diff/mount.js | 8 +++++++-
src/diff/patch.js | 5 +++++
src/internal.d.ts | 4 ++++
5 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/src/component.js b/src/component.js
index 719e42b7bb..ce5f60ca97 100644
--- a/src/component.js
+++ b/src/component.js
@@ -3,7 +3,6 @@ import options from './options';
import { createVNode, Fragment } from './create-element';
import { patch } from './diff/patch';
import { DIRTY_BIT, FORCE_UPDATE, MODE_UNMOUNTING } from './constants';
-import { getParentDom } from './tree';
/**
* Base Component class. Provides `setState()` and `forceUpdate()`, which
@@ -98,7 +97,7 @@ function rerender(internal) {
0
);
- patch(internal, vnode, getParentDom(internal));
+ patch(internal, vnode);
commitRoot(internal);
}
}
diff --git a/src/diff/children.js b/src/diff/children.js
index da7d61d83f..46fde13272 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -19,6 +19,7 @@ import { createInternal, getDomSibling, getParentDom } from '../tree';
* @param {import('../internal').ComponentChild[]} children The new children, represented as VNodes
*/
export function patchChildren(internal, children) {
+ /** @type {import('../internal').Internal[]} */
let oldChildren =
(internal._children && internal._children.slice()) || EMPTY_ARR;
@@ -192,8 +193,12 @@ function findMatchingIndex(
skewedIndex,
remainingOldChildren
) {
- const type = typeof childVNode === 'string' ? null : childVNode.type;
- const key = type !== null ? childVNode.key : UNDEFINED;
+ let type = null;
+ let key;
+ if (typeof childVNode !== 'string') {
+ type = childVNode.type;
+ key = childVNode.key;
+ }
let match = -1;
let x = skewedIndex - 1; // i - 1;
let y = skewedIndex + 1; // i + 1;
diff --git a/src/diff/mount.js b/src/diff/mount.js
index b26f45a829..a238515678 100644
--- a/src/diff/mount.js
+++ b/src/diff/mount.js
@@ -57,6 +57,7 @@ export function mount(internal, vnode, startDom) {
if (flags & TYPE_TEXT) {
// if hydrating (hydrate() or render() with replaceNode), find the matching child:
while (hydrateDom) {
+ // @ts-ignore-next-line ChildNode ~= PreactNode
nextDomSibling = hydrateDom.nextSibling;
if (hydrateDom.nodeType === 3) {
// if hydrating a Text node, ensure its text content is correct:
@@ -72,7 +73,10 @@ export function mount(internal, vnode, startDom) {
internal._dom = hydrateDom || document.createTextNode(props);
internal.flags &= RESET_MODE;
} else if (flags & TYPE_ELEMENT) {
- nextDomSibling = mountElement(internal, hydrateDom);
+ nextDomSibling = mountElement(
+ internal,
+ /** @type {import('../internal').PreactElement} */ (hydrateDom)
+ );
internal.flags &= RESET_MODE;
} else {
try {
@@ -269,6 +273,7 @@ function mountElement(internal, dom) {
}
break;
}
+ // @ts-ignore-next-line Element ~= PreactElement
dom = dom.nextElementSibling;
}
}
@@ -415,6 +420,7 @@ export function mountChildren(internal, children, startDom) {
// attributes & unused DOM)
while (startDom) {
i = startDom;
+ // @ts-ignore-next-line ChildNode ~= PreactNode
startDom = startDom.nextSibling;
i.remove();
}
diff --git a/src/diff/patch.js b/src/diff/patch.js
index 5e77241a8e..0b2a9c7bf0 100644
--- a/src/diff/patch.js
+++ b/src/diff/patch.js
@@ -44,6 +44,7 @@ export function patch(internal, vnode) {
// When passing through createElement it assigns the object
// constructor as undefined. This to prevent JSON-injection.
else if (vnode.constructor === UNDEFINED) {
+ // @ts-ignore vnode is never a string here
let newProps = vnode.props;
internal.props = newProps;
@@ -69,7 +70,9 @@ export function patch(internal, vnode) {
flags = internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR;
}
+ // @ts-ignore vnode is never a string here
let isSameVNode = vnode._vnodeId === internal._vnodeId;
+
if (!isSameVNode || (flags & FORCE_UPDATE) !== 0) {
if (flags & TYPE_ELEMENT) {
patchElement(
@@ -95,9 +98,11 @@ export function patch(internal, vnode) {
}
// Once we have successfully rendered the new VNode, copy it's ID over
+ // @ts-ignore vnode is never a string here
internal._vnodeId = vnode._vnodeId;
internal._prevRef = internal.ref;
+ // @ts-ignore vnode is never a string here
internal.ref = vnode.ref;
}
}
diff --git a/src/internal.d.ts b/src/internal.d.ts
index 7fc03d15dd..39c05540c6 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -120,6 +120,10 @@ export interface VNode
extends preact.VNode
{
// Redefine type here using our internal ComponentType type
type: string | ComponentType
;
props: P & { children: ComponentChildren };
+
+ // the ref types are duplicated internally, and we want to use the internal one here.
+ ref?: Ref | null;
+
/**
* Internal GUID for this VNode, used for fast equality checks.
* Note: h() allocates monotonic positive integer IDs, jsx() allocates negative.
From bd79906195b904f0720df7fdf726d276a96cb18e Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Sun, 29 May 2022 14:46:35 -0400
Subject: [PATCH 14/14] add test for portal siblings and missing TS properties
---
package.json | 1 +
src/internal.d.ts | 3 ++
test/_util/logCall.js | 5 ++
test/browser/portals.test.js | 96 +++++++++++++++++++++++++++++++++++-
4 files changed, 103 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 1b47f24bf6..b4a9f6a4d9 100644
--- a/package.json
+++ b/package.json
@@ -157,6 +157,7 @@
"varsIgnorePattern": "^h|React|_[0-9]?$"
}
],
+ "new-cap": 0,
"prefer-rest-params": 0,
"prefer-spread": 0,
"no-cond-assign": 0,
diff --git a/src/internal.d.ts b/src/internal.d.ts
index 39c05540c6..a6054c0649 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -106,6 +106,9 @@ export interface PreactElement extends HTMLElement {
// style: HTMLElement["style"]; // From HTMLElement
data?: string | number; // From Text node
+
+ _isControlled?: boolean;
+ _prevValue?: any;
}
export type PreactNode = PreactElement | Text;
diff --git a/test/_util/logCall.js b/test/_util/logCall.js
index 0eff2821b0..09dbecc640 100644
--- a/test/_util/logCall.js
+++ b/test/_util/logCall.js
@@ -29,6 +29,11 @@ export function logCall(obj, method) {
c += serialize(args[i]);
}
+ // insertBefore(x, null) === insertBefore(x, undefined)
+ if (method === 'insertBefore' && args[1] === undefined) {
+ args[1] = null;
+ }
+
// Normalize removeChild -> remove to keep output clean and readable
const operation =
method != 'removeChild'
diff --git a/test/browser/portals.test.js b/test/browser/portals.test.js
index 90f177d655..a80f1fdb58 100644
--- a/test/browser/portals.test.js
+++ b/test/browser/portals.test.js
@@ -3,12 +3,13 @@ import {
render,
createPortal,
Component,
- Fragment
+ Fragment,
+ hydrate
} from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { setupRerender, act } from 'preact/test-utils';
import { setupScratch, teardown } from '../_util/helpers';
-import { getLog, clearLog } from '../_util/logCall';
+import { getLog, clearLog, logCall } from '../_util/logCall';
/** @jsx createElement */
@@ -16,6 +17,10 @@ describe('Portal', () => {
/** @type {HTMLDivElement} */
let scratch;
let rerender;
+ let resetAppendChild;
+ let resetInsertBefore;
+ let resetRemoveChild;
+ let resetRemove;
beforeEach(() => {
scratch = setupScratch();
@@ -26,6 +31,20 @@ describe('Portal', () => {
teardown(scratch);
});
+ before(() => {
+ resetAppendChild = logCall(Element.prototype, 'appendChild');
+ resetInsertBefore = logCall(Element.prototype, 'insertBefore');
+ resetRemoveChild = logCall(Element.prototype, 'removeChild');
+ resetRemove = logCall(Element.prototype, 'remove');
+ });
+
+ after(() => {
+ resetAppendChild();
+ resetInsertBefore();
+ resetRemoveChild();
+ resetRemove();
+ });
+
it('should render into a different root node', () => {
let root = document.createElement('div');
document.body.appendChild(root);
@@ -40,6 +59,79 @@ describe('Portal', () => {
root.parentNode.removeChild(root);
});
+ it('should preserve mount order of non-portal siblings', () => {
+ let portals = document.createElement('portals');
+ scratch.appendChild(portals);
+
+ let main = document.createElement('main');
+ scratch.appendChild(main);
+
+ function Foo(props) {
+ return [
+
A
,
+ createPortal(B
, portals),
+ C
,
+ createPortal(D
, portals),
+ E
+ ];
+ }
+
+ render([, ], main);
+ clearLog();
+ render([, ], main);
+
+ expect(main.innerHTML).to.equal(
+ 'A
C
E
'
+ );
+ expect(portals.innerHTML).to.equal('B
D
');
+
+ // ignore Text node insertions:
+ const log = getLog().filter(t => !/#text/.test(t));
+
+ expect(log).to.deep.equal([
+ '.insertBefore(A, )',
+ '.insertBefore(B, Null)',
+ 'A.insertBefore(C, )',
+ 'B.insertBefore(D, Null)',
+ 'AC.insertBefore(E, )',
+ '.remove()'
+ ]);
+ });
+
+ it('should preserve hydration order of non-portal siblings', () => {
+ let portals = document.createElement('portals');
+ scratch.appendChild(portals);
+
+ let main = document.createElement('main');
+ scratch.appendChild(main);
+
+ main.innerHTML = 'A
C
E
';
+
+ function Foo(props) {
+ return [
+ A
,
+ createPortal(B
, portals),
+ C
,
+ createPortal(D
, portals),
+ E
+ ];
+ }
+
+ clearLog();
+ hydrate(, main);
+
+ expect(main.innerHTML).to.equal('A
C
E
');
+ expect(portals.innerHTML).to.equal('B
D
');
+
+ // ignore Text node insertions:
+ const log = getLog().filter(t => !/#text/.test(t));
+
+ expect(log).to.deep.equal([
+ '.insertBefore(B, Null)',
+ 'B.insertBefore(D, Null)'
+ ]);
+ });
+
it('should insert the portal', () => {
let setFalse;
function Foo(props) {