diff --git a/src/component.js b/src/component.js index ccdf9d569f..719e42b7bb 100644 --- a/src/component.js +++ b/src/component.js @@ -1,19 +1,9 @@ -import { commitRoot } from './diff/commit'; +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'; - -/** - * The render queue - * @type {import('./internal').RendererState} - */ -export const rendererState = { - _parentDom: null, - _context: {}, - _commitQueue: [] -}; +import { getParentDom } from './tree'; /** * Base Component class. Provides `setState()` and `forceUpdate()`, which @@ -108,10 +98,7 @@ function rerender(internal) { 0 ); - rendererState._context = getParentContext(internal); - rendererState._commitQueue = []; - rendererState._parentDom = 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 1f4bf9f961..58f8c5f9a6 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -4,13 +4,12 @@ import { MODE_SVG, UNDEFINED } from './constants'; -import { commitRoot } from './diff/commit'; +import { commitRoot } 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,13 +29,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; - if (rootInternal) { - patch(rootInternal, vnode); + patch(rootInternal, vnode, parentDom); } else { rootInternal = createInternal(vnode); @@ -55,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 9b26f85653..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 { rendererState } from '../component'; /** * 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; @@ -75,6 +75,7 @@ export function patchChildren(internal, children) { mount( childInternal, childVNode, + parentDom, getDomSibling(internal, skewedIndex) ); } @@ -85,14 +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) { @@ -102,7 +99,7 @@ export function patchChildren(internal, children) { // Perform insert of new dom if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore( + parentDom.insertBefore( childInternal._dom, getDomSibling(internal, skewedIndex) ); @@ -136,13 +133,9 @@ export function patchChildren(internal, children) { let nextSibling = getDomSibling(internal, skewedIndex + 1); if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore(childInternal._dom, nextSibling); + parentDom.insertBefore(childInternal._dom, nextSibling); } else { - insertComponentDom( - childInternal, - nextSibling, - rendererState._parentDom - ); + insertComponentDom(childInternal, nextSibling, parentDom); } } 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 6f80f355bc..b5d28aa1e2 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 { rendererState } from '../component'; /** * 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; @@ -43,11 +49,7 @@ export function renderFunctionComponent(internal, newVNode, componentContext) { } internal.flags &= ~DIRTY_BIT; if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() - ); + internal._context = Object.assign({}, context, c.getChildContext()); } return renderResult; @@ -57,9 +59,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; @@ -124,6 +133,7 @@ export function renderClassComponent(internal, newVNode, componentContext) { ) { c.state = c._nextState; internal.flags |= SKIP_CHILDREN; + internal.flags &= ~DIRTY_BIT; return; } @@ -147,11 +157,7 @@ export function renderClassComponent(internal, newVNode, componentContext) { c.state = c._nextState; if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() - ); + internal._context = Object.assign({}, context, c.getChildContext()); } if (!isNew) { diff --git a/src/diff/mount.js b/src/diff/mount.js index 9b1896b94a..0dc56afe0d 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -12,20 +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 { rendererState } 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').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} */ @@ -37,37 +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 = rendererState._parentDom; + let prevParentDom = parentDom; if (internal.flags & TYPE_ROOT) { - rendererState._parentDom = newVNode.props._parentDom; + parentDom = newVNode.props._parentDom; // Note: this is likely always true because we are inside mount() - if (rendererState._parentDom !== prevParentDom) { + if (parentDom !== prevParentDom) { startDom = null; } } - let prevContext = rendererState._context; + 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 && rendererState._context[tmp._id]; + let tmp = internal.type.contextType; + let provider = tmp && context[tmp._id]; let componentContext = tmp ? provider ? provider.props.value : tmp._defaultValue - : rendererState._context; + : 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 ); } @@ -87,17 +96,19 @@ export function mount(internal, newVNode, startDom) { renderResult = [renderResult]; } - nextDomSibling = mountChildren(internal, renderResult, startDom); + nextDomSibling = mountChildren( + internal, + renderResult, + parentDom, + startDom + ); } if (internal._commitCallbacks.length) { - rendererState._commitQueue.push(internal); + commitQueue.push(internal); } - if ( - internal.flags & TYPE_ROOT && - prevParentDom !== rendererState._parentDom - ) { + 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 @@ -105,11 +116,6 @@ export function mount(internal, newVNode, startDom) { // an existing tree. nextDomSibling = prevStartDom; } - - rendererState._parentDom = prevParentDom; - // In the event this subtree creates a new context for its children, restore - // the previous context for its siblings - rendererState._context = prevContext; } else { // @TODO: we could just assign this as internal.dom here let hydrateDom = @@ -258,14 +264,12 @@ function mountElement(internal, dom) { dom.innerHTML = newHtml.__html; } } else if (newChildren != null) { - const prevParentDom = rendererState._parentDom; - rendererState._parentDom = dom; mountChildren( internal, Array.isArray(newChildren) ? newChildren : [newChildren], + dom, isNew ? null : dom.firstChild ); - rendererState._parentDom = prevParentDom; } // (as above, don't diff props during hydration) @@ -282,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, @@ -306,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; @@ -319,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 - rendererState._parentDom.insertBefore(newDom, startDom); + parentDom.insertBefore(newDom, startDom); } if (childInternal.ref) { diff --git a/src/diff/patch.js b/src/diff/patch.js index c5581deb03..7aaeb6f6cb 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -15,20 +15,20 @@ import { MODE_HYDRATE, MODE_PENDING_ERROR, MODE_RERENDERING_ERROR, - SKIP_CHILDREN, - DIRTY_BIT + SKIP_CHILDREN } from '../constants'; -import { getDomSibling } from '../tree'; +import { getDomSibling, getParentContext } from '../tree'; import { mountChildren } from './mount'; import { Fragment } from '../create-element'; -import { rendererState } from '../component'; +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) { @@ -50,23 +50,21 @@ 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 = parentDom; if (flags & TYPE_ROOT) { - rendererState._parentDom = vnode.props._parentDom; + parentDom = vnode.props._parentDom; - if (internal.props._parentDom !== vnode.props._parentDom) { + if (internal.props._parentDom !== parentDom) { let nextSibling = - rendererState._parentDom == prevParentDom - ? getDomSibling(internal) - : null; - insertComponentDom(internal, nextSibling, rendererState._parentDom); + 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); + patchElement(internal, internal.props, vnode.props, flags); internal.props = vnode.props; } } else { @@ -79,30 +77,33 @@ export function patch(internal, vnode) { try { let renderResult; - let prevContext = rendererState._context; - if (internal._vnodeId === vnode._vnodeId) { + if (vnode._vnodeId === internal._vnodeId) { 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 && rendererState._context[tmp._id]; + let tmp = internal.type.contextType; + let provider = tmp && context[tmp._id]; let componentContext = tmp ? provider ? provider.props.value : tmp._defaultValue - : rendererState._context; + : context; - if (flags & TYPE_CLASS) { + if (internal.flags & TYPE_CLASS) { renderResult = renderClassComponent( internal, vnode, + context, componentContext ); } else { renderResult = renderFunctionComponent( internal, vnode, + context, componentContext ); } @@ -121,33 +122,28 @@ export function patch(internal, vnode) { } } + // handle sCU bailout. See https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8 if (internal.flags & SKIP_CHILDREN) { - internal._component.props = internal.props = vnode.props; - if (vnode._vnodeId !== internal._vnodeId) { - internal.flags &= ~DIRTY_BIT; - } + // Note: SKIP_CHILDREN gets unset by the `RESET_MODE` inversion below. + // internal.flags &= ~SKIP_CHILDREN; + 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); - - mountChildren(internal, renderResult, siblingDom); + let siblingDom; + if (flags & MODE_HYDRATE) { + siblingDom = flags & MODE_SUSPENDED ? internal._dom : null; + } else { + siblingDom = getDomSibling(internal); + } + + mountChildren(internal, renderResult, parentDom, siblingDom); } else { - patchChildren(internal, renderResult); + patchChildren(internal, renderResult, parentDom); } if (internal._commitCallbacks.length) { - rendererState._commitQueue.push(internal); + commitQueue.push(internal); } - - rendererState._parentDom = prevParentDom; - // In the event this subtree creates a new context for its children, restore - // the previous context for its siblings - rendererState._context = prevContext; } catch (e) { // @TODO: assign a new VNode ID here? Or NaN? // newVNode._vnodeId = 0; @@ -171,13 +167,13 @@ export function patch(internal, vnode) { /** * 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 = vnode.props, - isSvg = internal.flags & MODE_SVG, + isSvg = flags & MODE_SVG, i, value, tmp, @@ -219,13 +215,11 @@ function patchElement(internal, vnode) { internal._children = null; } else { if (oldHtml) dom.innerHTML = ''; - const prevParentDom = rendererState._parentDom; - rendererState._parentDom = dom; patchChildren( internal, - newChildren && Array.isArray(newChildren) ? newChildren : [newChildren] + newChildren && Array.isArray(newChildren) ? newChildren : [newChildren], + dom ); - rendererState._parentDom = 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..78c43fe621 --- /dev/null +++ b/src/diff/renderer.js @@ -0,0 +1,30 @@ +import options from '../options'; + +/** + * 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 = []; + +/** + * @param {import('../internal').Internal} rootInternal + */ +export function commitRoot(rootInternal) { + let currentQueue = 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