From 8ea55cefd53f6a47a06b2379e94eca45f4d351e6 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Tue, 12 Apr 2022 17:06:10 -0400 Subject: [PATCH 1/6] 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 2/6] 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 1db7aad674f96feebce4aafbed4073ae8a8948b6 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Fri, 22 Apr 2022 11:43:59 -0400 Subject: [PATCH 3/6] a couple small tweaks to patch for size/perf --- src/diff/component.js | 2 + src/diff/patch.js | 116 +++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/diff/component.js b/src/diff/component.js index e6d226c48c..af20fcd979 100644 --- a/src/diff/component.js +++ b/src/diff/component.js @@ -36,6 +36,7 @@ export function renderFunctionComponent( if (newVNode && newVNode._vnodeId === internal._vnodeId) { c.props = newProps; internal.flags |= SKIP_CHILDREN; + internal.flags &= ~DIRTY_BIT; return; } @@ -141,6 +142,7 @@ export function renderClassComponent( c.props = newProps; c.state = c._nextState; internal.flags |= SKIP_CHILDREN; + internal.flags &= ~DIRTY_BIT; return; } diff --git a/src/diff/patch.js b/src/diff/patch.js index a2922fb39e..e69c1bcbce 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -55,7 +55,7 @@ export function patch(internal, vnode, parentDom) { if (flags & TYPE_ROOT) { parentDom = vnode.props._parentDom; - if (internal.props._parentDom !== vnode.props._parentDom) { + if (internal.props._parentDom !== parentDom) { let nextSibling = parentDom == prevParentDom ? getDomSibling(internal) : null; insertComponentDom(internal, nextSibling, parentDom); @@ -65,74 +65,76 @@ export function patch(internal, vnode, parentDom) { if (flags & TYPE_ELEMENT) { if (vnode._vnodeId !== internal._vnodeId) { // @ts-ignore dom is a PreactElement here - patchElement(internal, vnode); + 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 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; + 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; + } + try { let renderResult; - - if (internal.flags & TYPE_CLASS) { - renderResult = renderClassComponent( - internal, - vnode, - context, - componentContext - ); + if (vnode._vnodeId === internal._vnodeId) { + internal.flags |= SKIP_CHILDREN; } 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; + 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 (!Array.isArray(renderResult)) { + + 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]; } - } else { - renderResult = [renderResult]; } + // handle sCU bailout. See https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8 if (internal.flags & SKIP_CHILDREN) { + // Note: SKIP_CHILDREN gets unset by the `RESET_MODE` inversion below. + // 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; - } + internal._component.props = vnode.props; } 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); @@ -167,13 +169,13 @@ export function patch(internal, vnode, parentDom) { /** * 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 249fbe36f7c1708488695004c298963b5d21cb2d Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Fri, 22 Apr 2022 11:52:40 -0400 Subject: [PATCH 4/6] fix lint issue --- src/diff/patch.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/diff/patch.js b/src/diff/patch.js index e69c1bcbce..374e1666ae 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -15,8 +15,7 @@ import { MODE_HYDRATE, MODE_PENDING_ERROR, MODE_RERENDERING_ERROR, - SKIP_CHILDREN, - DIRTY_BIT + SKIP_CHILDREN } from '../constants'; import { getDomSibling, getParentContext } from '../tree'; import { mountChildren } from './mount'; From a537cfc88bc9c69a39b6a3d9483cb024430547d4 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Mon, 25 Apr 2022 11:47:00 -0400 Subject: [PATCH 5/6] tweak: access contextType from internal instead of vnode --- src/diff/mount.js | 2 +- src/diff/patch.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diff/mount.js b/src/diff/mount.js index ca9e4d9996..0dc56afe0d 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -53,7 +53,7 @@ export function mount(internal, newVNode, parentDom, startDom) { // 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 tmp = internal.type.contextType; let provider = tmp && context[tmp._id]; let componentContext = tmp ? provider diff --git a/src/diff/patch.js b/src/diff/patch.js index 46dcbfe462..83647ee96d 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -84,7 +84,7 @@ export function patch(internal, vnode, parentDom) { // 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 tmp = internal.type.contextType; let provider = tmp && context[tmp._id]; let componentContext = tmp ? provider From 79c1628facce5c0fa86cb1210def89127a780f90 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Mon, 25 Apr 2022 12:35:13 -0400 Subject: [PATCH 6/6] simplify siblingDom calc and reduce size --- src/diff/patch.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/diff/patch.js b/src/diff/patch.js index 83647ee96d..7aaeb6f6cb 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -129,13 +129,12 @@ export function patch(internal, vnode, parentDom) { internal.props = vnode.props; internal._component.props = vnode.props; } else if (internal._children == null) { - let siblingDom = - (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === - (MODE_HYDRATE | MODE_SUSPENDED) - ? internal._dom - : 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 {