From 8ea55cefd53f6a47a06b2379e94eca45f4d351e6 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Tue, 12 Apr 2022 17:06:10 -0400 Subject: [PATCH 01/14] checkpoint: renderer state refactor to accessors --- src/component.js | 23 +++++++--------- src/create-root.js | 13 ++++----- src/diff/children.js | 24 +++++------------ src/diff/commit.js | 25 ----------------- src/diff/component.js | 34 +++++++++++------------- src/diff/mount.js | 62 +++++++++++++++++++------------------------ src/diff/patch.js | 51 +++++++++++++++++------------------ src/diff/renderer.js | 55 ++++++++++++++++++++++++++++++++++++++ src/internal.d.ts | 8 ++---- 9 files changed, 145 insertions(+), 150 deletions(-) delete mode 100644 src/diff/commit.js create mode 100644 src/diff/renderer.js diff --git a/src/component.js b/src/component.js index ccdf9d569f..22e9dc8e64 100644 --- a/src/component.js +++ b/src/component.js @@ -1,20 +1,14 @@ -import { commitRoot } from './diff/commit'; +import { + commitRoot, + setCurrentContext, + setCurrentParentDom +} 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'; -/** - * The render queue - * @type {import('./internal').RendererState} - */ -export const rendererState = { - _parentDom: null, - _context: {}, - _commitQueue: [] -}; - /** * Base Component class. Provides `setState()` and `forceUpdate()`, which * trigger rendering @@ -108,9 +102,10 @@ function rerender(internal) { 0 ); - rendererState._context = getParentContext(internal); - rendererState._commitQueue = []; - rendererState._parentDom = getParentDom(internal); + // set up renderer state + setCurrentContext(getParentContext(internal)); + setCurrentParentDom(getParentDom(internal)); + patch(internal, vnode); commitRoot(internal); } diff --git a/src/create-root.js b/src/create-root.js index 1f4bf9f961..2edfe47a96 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -4,13 +4,16 @@ import { MODE_SVG, UNDEFINED } from './constants'; -import { commitRoot } from './diff/commit'; +import { + commitRoot, + setCurrentContext, + setCurrentParentDom +} from './diff/renderer'; import { createElement, Fragment } from './create-element'; import options from './options'; import { mount } from './diff/mount'; import { patch } from './diff/patch'; import { createInternal } from './tree'; -import { rendererState } from './component'; /** * @@ -30,10 +33,8 @@ export function createRoot(parentDom) { firstChild = /** @type {import('./internal').PreactElement} */ (parentDom.firstChild); - rendererState._context = {}; - // List of effects that need to be called after diffing: - rendererState._commitQueue = []; - rendererState._parentDom = parentDom; + setCurrentContext({}); + setCurrentParentDom(parentDom); if (rootInternal) { patch(rootInternal, vnode); diff --git a/src/diff/children.js b/src/diff/children.js index 9b26f85653..0d5edd837f 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -12,7 +12,7 @@ import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; import { createInternal, getDomSibling } from '../tree'; -import { rendererState } from '../component'; +import { getCurrentParentDom } from './renderer'; /** * Update an internal with new children. @@ -72,11 +72,7 @@ export function patchChildren(internal, children) { childInternal = createInternal(childVNode, internal); // We are mounting a new VNode - mount( - childInternal, - childVNode, - 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. @@ -85,11 +81,7 @@ 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, childInternal._dom); } else { // Morph the old element into the new one, but don't append it to the dom yet patch(childInternal, childVNode); @@ -102,7 +94,7 @@ export function patchChildren(internal, children) { // Perform insert of new dom if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore( + getCurrentParentDom().insertBefore( childInternal._dom, getDomSibling(internal, skewedIndex) ); @@ -136,13 +128,9 @@ export function patchChildren(internal, children) { let nextSibling = getDomSibling(internal, skewedIndex + 1); if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore(childInternal._dom, nextSibling); + getCurrentParentDom().insertBefore(childInternal._dom, nextSibling); } else { - insertComponentDom( - childInternal, - nextSibling, - rendererState._parentDom - ); + insertComponentDom(childInternal, nextSibling, getCurrentParentDom()); } } diff --git a/src/diff/commit.js b/src/diff/commit.js deleted file mode 100644 index 0b74ef671b..0000000000 --- a/src/diff/commit.js +++ /dev/null @@ -1,25 +0,0 @@ -import { rendererState } from '../component'; -import options from '../options'; - -/** - * @param {import('../internal').Internal} rootInternal - */ -export function commitRoot(rootInternal) { - let commitQueue = [].concat(rendererState._commitQueue); - rendererState._commitQueue = []; - - if (options._commit) options._commit(rootInternal, commitQueue); - - commitQueue.some(internal => { - try { - // @ts-ignore Reuse the root variable here so the type changes - commitQueue = internal._commitCallbacks.length; - // @ts-ignore See above ts-ignore comment - while (commitQueue--) { - internal._commitCallbacks.shift()(); - } - } catch (e) { - options._catchError(e, internal); - } - }); -} diff --git a/src/diff/component.js b/src/diff/component.js index 0c4356a940..eff6cacc47 100644 --- a/src/diff/component.js +++ b/src/diff/component.js @@ -1,6 +1,6 @@ import options from '../options'; import { DIRTY_BIT, FORCE_UPDATE, SKIP_CHILDREN } from '../constants'; -import { rendererState } from '../component'; +import { getCurrentContext, setCurrentContext } from './renderer'; /** * Render a function component @@ -8,11 +8,7 @@ import { rendererState } from '../component'; * @param {import('../internal').VNode} newVNode The new virtual node * @returns {import('../internal').ComponentChildren} the component's children */ -export function renderFunctionComponent( - internal, - newVNode, - componentContext -) { +export function renderFunctionComponent(internal, newVNode, componentContext) { /** @type {import('../internal').Component} */ let c; @@ -53,10 +49,12 @@ export function renderFunctionComponent( } internal.flags &= ~DIRTY_BIT; if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() + setCurrentContext( + (internal._context = Object.assign( + {}, + getCurrentContext(), + c.getChildContext() + )) ); } @@ -69,11 +67,7 @@ export function renderFunctionComponent( * @param {import('../internal').VNode} newVNode The new virtual node * @returns {import('../internal').ComponentChildren} the component's children */ -export function renderClassComponent( - internal, - newVNode, - componentContext -) { +export function renderClassComponent(internal, newVNode, componentContext) { /** @type {import('../internal').Component} */ let c; let isNew, oldProps, oldState, snapshot; @@ -163,10 +157,12 @@ export function renderClassComponent( c.state = c._nextState; if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() + setCurrentContext( + (internal._context = Object.assign( + {}, + getCurrentContext(), + c.getChildContext() + )) ); } diff --git a/src/diff/mount.js b/src/diff/mount.js index 951c8e4585..734cbc23ca 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -17,7 +17,14 @@ import { setProperty } from './props'; import { renderClassComponent, renderFunctionComponent } from './component'; import { createInternal } from '../tree'; import options from '../options'; -import { rendererState } from '../component'; +import { + commitQueue, + getCurrentContext, + getCurrentParentDom, + setCurrentContext, + setCurrentParentDom +} from './renderer'; + /** * Diff two virtual nodes and apply proper changes to the DOM * @param {import('../internal').Internal} internal The Internal node to mount @@ -37,37 +44,34 @@ 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 = rendererState._parentDom; + let prevParentDom = getCurrentParentDom(); if (internal.flags & TYPE_ROOT) { - rendererState._parentDom = newVNode.props._parentDom; + let newParentDom = newVNode.props._parentDom; + setCurrentParentDom(newParentDom); // Note: this is likely always true because we are inside mount() - if (rendererState._parentDom !== prevParentDom) { + if (newParentDom !== prevParentDom) { startDom = null; } } - let prevContext = rendererState._context; + let prevContext = getCurrentContext(); // 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 && rendererState._context[tmp._id]; + let provider = tmp && prevContext[tmp._id]; let componentContext = tmp ? provider ? provider.props.value : tmp._defaultValue - : rendererState._context; + : prevContext; if (provider) provider._subs.add(internal); let renderResult; if (internal.flags & TYPE_CLASS) { - renderResult = renderClassComponent( - internal, - null, - componentContext - ); + renderResult = renderClassComponent(internal, null, componentContext); } else { renderResult = renderFunctionComponent( internal, @@ -91,23 +95,19 @@ export function mount(internal, newVNode, startDom) { renderResult = [renderResult]; } - nextDomSibling = mountChildren( - internal, - renderResult, - startDom - ); + nextDomSibling = mountChildren(internal, renderResult, startDom); } if ( internal._commitCallbacks != null && internal._commitCallbacks.length ) { - rendererState._commitQueue.push(internal); + commitQueue.push(internal); } if ( internal.flags & TYPE_ROOT && - prevParentDom !== rendererState._parentDom + prevParentDom !== getCurrentParentDom() ) { // 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 @@ -117,10 +117,10 @@ export function mount(internal, newVNode, startDom) { nextDomSibling = prevStartDom; } - rendererState._parentDom = prevParentDom; + setCurrentParentDom(prevParentDom); // In the event this subtree creates a new context for its children, restore // the previous context for its siblings - rendererState._context = prevContext; + setCurrentContext(prevContext); } else { // @TODO: we could just assign this as internal.dom here let hydrateDom = @@ -269,14 +269,14 @@ function mountElement(internal, dom) { dom.innerHTML = newHtml.__html; } } else if (newChildren != null) { - const prevParentDom = rendererState._parentDom; - rendererState._parentDom = dom; + const prevParentDom = getCurrentParentDom(); + setCurrentParentDom(dom); mountChildren( internal, Array.isArray(newChildren) ? newChildren : [newChildren], isNew ? null : dom.firstChild ); - rendererState._parentDom = prevParentDom; + setCurrentParentDom(prevParentDom); } // (as above, don't diff props during hydration) @@ -295,11 +295,7 @@ function mountElement(internal, dom) { * @param {import('../internal').ComponentChild[]} children * @param {import('../internal').PreactNode} startDom */ -export function mountChildren( - internal, - children, - startDom -) { +export function mountChildren(internal, children, startDom) { let internalChildren = (internal._children = []), i, childVNode, @@ -321,11 +317,7 @@ export function mountChildren( 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, startDom); newDom = childInternal._dom; @@ -338,7 +330,7 @@ export function mountChildren( // The DOM the diff should begin with is now startDom (since we inserted // newDom before startDom) so ignore mountedNextChild and continue with // startDom - rendererState._parentDom.insertBefore(newDom, startDom); + getCurrentParentDom().insertBefore(newDom, startDom); } if (childInternal.ref) { diff --git a/src/diff/patch.js b/src/diff/patch.js index c286c96189..344b8a8e78 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -21,7 +21,13 @@ import { import { getDomSibling } from '../tree'; import { mountChildren } from './mount'; import { Fragment } from '../create-element'; -import { rendererState } from '../component'; +import { + commitQueue, + getCurrentContext, + getCurrentParentDom, + setCurrentContext, + setCurrentParentDom +} from './renderer'; /** * Diff two virtual nodes and apply proper changes to the DOM @@ -50,16 +56,15 @@ 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 = rendererState._parentDom; + let prevParentDom = getCurrentParentDom(); if (flags & TYPE_ROOT) { - rendererState._parentDom = vnode.props._parentDom; + let newParentDom = vnode.props._parentDom; + setCurrentParentDom(newParentDom); if (internal.props._parentDom !== vnode.props._parentDom) { let nextSibling = - rendererState._parentDom == prevParentDom - ? getDomSibling(internal) - : null; - insertComponentDom(internal, nextSibling, rendererState._parentDom); + newParentDom == prevParentDom ? getDomSibling(internal) : null; + insertComponentDom(internal, nextSibling, newParentDom); } } @@ -77,26 +82,22 @@ export function patch(internal, vnode) { internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR; } - let prevContext = rendererState._context; + let prevContext = getCurrentContext(); // 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 && rendererState._context[tmp._id]; + let provider = tmp && prevContext[tmp._id]; let componentContext = tmp ? provider ? provider.props.value : tmp._defaultValue - : rendererState._context; + : prevContext; let isNew = !internal || !internal._component; let renderResult; if (internal.flags & TYPE_CLASS) { - renderResult = renderClassComponent( - internal, - vnode, - componentContext - ); + renderResult = renderClassComponent(internal, vnode, componentContext); } else { renderResult = renderFunctionComponent( internal, @@ -134,11 +135,7 @@ export function patch(internal, vnode) { ? null : getDomSibling(internal); - mountChildren( - internal, - renderResult, - siblingDom - ); + mountChildren(internal, renderResult, siblingDom); } else { patchChildren(internal, renderResult); } @@ -147,13 +144,13 @@ export function patch(internal, vnode) { internal._commitCallbacks != null && internal._commitCallbacks.length ) { - rendererState._commitQueue.push(internal); + commitQueue.push(internal); } - rendererState._parentDom = prevParentDom; + setCurrentParentDom(prevParentDom); // In the event this subtree creates a new context for its children, restore // the previous context for its siblings - rendererState._context = prevContext; + setCurrentContext(prevContext); } catch (e) { // @TODO: assign a new VNode ID here? Or NaN? // newVNode._vnodeId = 0; @@ -225,13 +222,13 @@ function patchElement(internal, vnode) { internal._children = null; } else { if (oldHtml) dom.innerHTML = ''; - const prevParentDom = rendererState._parentDom; - rendererState._parentDom = dom; + const prevParentDom = getCurrentParentDom(); + setCurrentParentDom(dom); patchChildren( internal, - newChildren && Array.isArray(newChildren) ? newChildren : [newChildren], + newChildren && Array.isArray(newChildren) ? newChildren : [newChildren] ); - rendererState._parentDom = prevParentDom; + setCurrentParentDom(prevParentDom); } if (newProps.checked != null && dom._isControlled) { diff --git a/src/diff/renderer.js b/src/diff/renderer.js new file mode 100644 index 0000000000..4a53f0e63b --- /dev/null +++ b/src/diff/renderer.js @@ -0,0 +1,55 @@ +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); + commitQueue = []; + + if (options._commit) options._commit(rootInternal, currentQueue); + + currentQueue.some(internal => { + try { + // @ts-ignore Reuse the root variable here so the type changes + currentQueue = internal._commitCallbacks.length; + // @ts-ignore See above ts-ignore comment + while (currentQueue--) { + internal._commitCallbacks.shift()(); + } + } catch (e) { + options._catchError(e, internal); + } + }); +} diff --git a/src/internal.d.ts b/src/internal.d.ts index 19e57fff63..7fc03d15dd 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -45,14 +45,10 @@ export interface Options extends preact.Options { _internal?(internal: Internal, vnode: VNode | string): void; } -export type RendererState = { - _context: Record; - _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) {