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/component.js b/src/component.js index ccdf9d569f..ce5f60ca97 100644 --- a/src/component.js +++ b/src/component.js @@ -1,19 +1,8 @@ -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: [] -}; /** * Base Component class. Provides `setState()` and `forceUpdate()`, which @@ -108,9 +97,6 @@ function rerender(internal) { 0 ); - rendererState._context = getParentContext(internal); - rendererState._commitQueue = []; - rendererState._parentDom = getParentDom(internal); patch(internal, vnode); commitRoot(internal); } diff --git a/src/create-root.js b/src/create-root.js index 1f4bf9f961..228b28bf40 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,11 +29,6 @@ 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); } else { diff --git a/src/diff/children.js b/src/diff/children.js index 9b26f85653..46fde13272 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -11,8 +11,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getDomSibling } from '../tree'; -import { rendererState } from '../component'; +import { createInternal, getDomSibling, getParentDom } from '../tree'; /** * Update an internal with new children. @@ -20,12 +19,20 @@ import { rendererState } from '../component'; * @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; 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,11 +79,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 +88,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 +101,7 @@ export function patchChildren(internal, children) { // Perform insert of new dom if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore( + (parentDom || (parentDom = getParentDom(internal))).insertBefore( childInternal._dom, getDomSibling(internal, skewedIndex) ); @@ -136,12 +135,15 @@ export function patchChildren(internal, children) { let nextSibling = getDomSibling(internal, skewedIndex + 1); if (childInternal.flags & TYPE_DOM) { - rendererState._parentDom.insertBefore(childInternal._dom, nextSibling); + (parentDom || (parentDom = getParentDom(internal))).insertBefore( + childInternal._dom, + nextSibling + ); } else { insertComponentDom( childInternal, nextSibling, - rendererState._parentDom + parentDom || (parentDom = getParentDom(internal)) ); } } @@ -191,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/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/mount.js b/src/diff/mount.js index 3590530a71..a238515678 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -13,91 +13,227 @@ import { MODE_SVG, DIRTY_BIT } from '../constants'; +import options from '../options'; import { normalizeToVNode, Fragment } from '../create-element'; import { setProperty } from './props'; -import { createInternal } from '../tree'; -import options from '../options'; -import { rendererState } from '../component'; +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} newVNode The new virtual node + * @param {import('../internal').VNode | string} vnode The new virtual node * @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) { - if (options._diff) options._diff(internal, newVNode); +export function mount(internal, vnode, startDom) { + if (options._diff) options._diff(internal, vnode); + + let flags = internal.flags; + let props = internal.props; /** @type {import('../internal').PreactNode} */ - let nextDomSibling, prevStartDom; - - 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 prevParentDom = rendererState._parentDom; - if (internal.flags & TYPE_ROOT) { - rendererState._parentDom = newVNode.props._parentDom; - - // Note: this is likely always true because we are inside mount() - if (rendererState._parentDom !== prevParentDom) { - prevStartDom = startDom; - startDom = null; + let nextDomSibling; + + // @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; + if (flags & TYPE_ROOT) { + // 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 (props._parentDom !== implicitParentDom) { + prevStartDom = startDom; + startDom = null; + } + } + + 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: + if (hydrateDom.data != props) { + hydrateDom.data = props; } + break; } + hydrateDom = nextDomSibling; + } - let prevContext = rendererState._context; - - nextDomSibling = mountComponent(internal, startDom); + // @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, + /** @type {import('../internal').PreactElement} */ (hydrateDom) + ); + internal.flags &= RESET_MODE; + } else { + try { + nextDomSibling = mountComponent(internal, props, startDom, flags); 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; - } else { - // @TODO: we could just assign this as internal.dom here - let hydrateDom = - internal.flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE) - ? startDom - : null; + // 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; - nextDomSibling = mountElement(internal, hydrateDom); + 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 + } + options._catchError(e, internal); } + } - if (options.diffed) options.diffed(internal); + // internal.flags &= RESET_MODE; - // 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 + if (options.diffed) options.diffed(internal); + + // 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').PreactNode} startDom + * @param {import('../internal').Internal['flags']} flags + */ +function mountComponent(internal, props, 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); } - options._catchError(e, internal); + + Object.assign( + inst._nextState, + type.getDerivedStateFromProps(props, inst._nextState) + ); + } else if (inst.componentWillMount != null) { + inst.componentWillMount(); } - return prevStartDom || nextDomSibling; + // 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; + + // note: disable repeat render invocation for class components + if (flags & TYPE_CLASS) { + internal.flags &= ~DIRTY_BIT; + if (renderHook) renderHook(internal); + 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; + } + } + } + + // 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, 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; @@ -105,126 +241,116 @@ 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; + /** @type {import('../internal').PreactNode} */ + let hydrateChild = null; + /** @type {import('../internal').PreactNode} */ + 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) { + // @ts-ignore-next ChildNode ~= PreactNode + hydrateChild = dom.firstChild; + // @ts-ignore-next ChildNode ~= PreactNode + 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; + } + // @ts-ignore-next-line Element ~= PreactElement + 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) { - const prevParentDom = rendererState._parentDom; - rendererState._parentDom = dom; - mountChildren( - internal, - Array.isArray(newChildren) ? newChildren : [newChildren], - isNew ? null : dom.firstChild - ); - rendererState._parentDom = prevParentDom; + // 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], + hydrateChild + ); + } - // (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; } /** @@ -239,7 +365,8 @@ export function mountChildren(internal, children, startDom) { childVNode, childInternal, newDom, - mountedNextChild; + mountedNextChild, + parentDom; for (i = 0; i < children.length; i++) { childVNode = normalizeToVNode(children[i]); @@ -265,10 +392,12 @@ export function mountChildren(internal, children, 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 - rendererState._parentDom.insertBefore(newDom, startDom); + parent.insertBefore(newDom, startDom); } if (childInternal.ref) { @@ -291,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(); } @@ -298,121 +428,3 @@ export function mountChildren(internal, children, startDom) { return startDom; } - -/** - * @param {import('../internal').Internal} internal The component's backing Internal node - * @param {import('../internal').PreactNode} startDom the preceding node - * @returns {import('../internal').PreactNode} the component's children - */ -function mountComponent(internal, startDom) { - /** @type {import('../internal').Component} */ - let c; - let type = /** @type {import('../internal').ComponentType} */ (internal.type); - let newProps = internal.props; - - // 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 && rendererState._context[tmp._id]; - let componentContext = tmp - ? provider - ? provider.props.value - : tmp._defaultValue - : rendererState._context; - - if (provider) provider._subs.add(internal); - - if (internal.flags & TYPE_CLASS) { - // @ts-ignore `type` is a class component constructor - c = new type(newProps, componentContext); - } else { - c = { - props: newProps, - context: componentContext, - forceUpdate: internal.rerender.bind(null, internal) - }; - } - - c._internal = internal; - internal._component = c; - internal.flags |= DIRTY_BIT; - - if (!c.state) c.state = {}; - 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) - ); - } - - 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)); - } - - c.context = componentContext; - internal.props = c.props = newProps; - c.state = c._nextState; - - let renderHook = options._render; - if (renderHook) renderHook(internal); - - let counter = 0, - renderResult; - - while (counter++ < 25) { - internal.flags &= ~DIRTY_BIT; - if (renderHook) renderHook(internal); - if (internal.flags & TYPE_CLASS) { - renderResult = c.render(c.props, c.state, c.context); - // note: disable repeat render invocation for class components - break; - } else { - renderResult = type.call(c, c.props, c.context); - } - - if (!(internal.flags & DIRTY_BIT)) { - break; - } - } - - // Handle setState called in render, see #2553 - c.state = c._nextState; - - if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() - ); - } - - if (renderResult == null) { - return startDom; - } - 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]; - } - - return mountChildren(internal, renderResult, startDom); -} diff --git a/src/diff/patch.js b/src/diff/patch.js index e4102b0d35..9539933556 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -14,14 +14,13 @@ import { MODE_HYDRATE, MODE_PENDING_ERROR, MODE_RERENDERING_ERROR, - SKIP_CHILDREN, DIRTY_BIT, FORCE_UPDATE } from '../constants'; -import { getDomSibling } from '../tree'; +import { getDomSibling, getParentContext, getParentDom } 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 @@ -29,120 +28,101 @@ import { rendererState } from '../component'; * @param {import('../internal').VNode | string} vnode The new virtual node */ export function patch(internal, vnode) { + let diffHook; + if ((diffHook = options._diff)) diffHook(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); - - // 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; - if (flags & TYPE_ROOT) { - rendererState._parentDom = vnode.props._parentDom; - - if (internal.props._parentDom !== vnode.props._parentDom) { - let nextSibling = - rendererState._parentDom == prevParentDom - ? getDomSibling(internal) - : null; - insertComponentDom(internal, nextSibling, rendererState._parentDom); + else if (vnode.constructor === UNDEFINED) { + // @ts-ignore vnode is never a string here + 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? + if (flags & TYPE_ROOT) { + 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 == getParentDom(internal._parent) + ? 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); - internal.props = vnode.props; - } - } else { + // Switch from MODE_PENDING_ERROR to MODE_RERENDERING_ERROR: if (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; + flags = internal.flags ^= MODE_PENDING_ERROR | MODE_RERENDERING_ERROR; } - try { - let renderResult; - let prevContext = rendererState._context; - if (internal._vnodeId === vnode._vnodeId) { - internal.flags |= SKIP_CHILDREN; + // @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( + internal, + // @ts-ignore _dom is a PreactElement here + internal._dom, + prevProps, + newProps, + flags + ); } else { - renderResult = patchComponent(internal, vnode); - } - - if (internal.flags & SKIP_CHILDREN) { - internal._component.props = internal.props = vnode.props; - if (vnode._vnodeId !== internal._vnodeId) { - internal.flags &= ~DIRTY_BIT; + patchComponent( + internal, + internal._component, + prevProps, + newProps, + flags + ); + + if (internal._commitCallbacks.length) { + commitQueue.push(internal); } - } 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); - } else { - patchChildren(internal, renderResult); } - if (internal._commitCallbacks.length) { - rendererState._commitQueue.push(internal); - } + // 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; - 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; - internal.flags |= e.then ? MODE_SUSPENDED : MODE_ERRORED; - options._catchError(e, internal); + internal._prevRef = internal.ref; + // @ts-ignore vnode is never a string here + 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; - - internal._prevRef = internal.ref; - internal.ref = vnode.ref; + if ((diffHook = options.diffed)) diffHook(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 {import('../internal').PreactElement} dom + * @param {any} oldProps + * @param {any} newProps + * @param {import('../internal').Internal['flags']} flags */ -function patchElement(internal, vnode) { - let dom = /** @type {import('../internal').PreactElement} */ (internal._dom), - oldProps = internal.props, - newProps = vnode.props, - isSvg = internal.flags & MODE_SVG, +function patchElement(internal, dom, oldProps, newProps, flags) { + let isSvg = flags & MODE_SVG, i, value, tmp, @@ -184,13 +164,10 @@ 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] ); - rendererState._parentDom = prevParentDom; } if (newProps.checked != null && dom._isControlled) { @@ -201,125 +178,148 @@ function patchElement(internal, vnode) { } /** - * @param {import('../internal').Internal} internal The component's backing Internal node - * @param {import('../internal').VNode} newVNode The new virtual node - * @returns {import('../internal').ComponentChildren} the component's children + * @param {import('../internal').Internal} internal + * @param {import('../internal').Component} inst + * @param {any} prevProps + * @param {any} newProps + * @param {import('../internal').Internal['flags']} flags */ -function patchComponent(internal, newVNode) { +function patchComponent(internal, inst, prevProps, newProps, flags) { let type = /** @type {import('../internal').ComponentType} */ (internal.type); - let snapshot, - c = internal._component, - newProps = newVNode.props; + + 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 = newVNode.type.contextType; - let provider = tmp && rendererState._context[tmp._id]; + let tmp = type.contextType; + let provider = tmp && context[tmp._id]; let componentContext = tmp ? provider ? provider.props.value : tmp._defaultValue - : rendererState._context; + : context; + // inst.context = componentContext; - if (c._nextState == null) { - c._nextState = c.state; - } + try { + if (type.getDerivedStateFromProps != null) { + if (inst._nextState === prevState) { + inst._nextState = Object.assign({}, inst._nextState); + } - if (type.getDerivedStateFromProps != null) { - if (c._nextState == c.state) { - c._nextState = Object.assign({}, c._nextState); + Object.assign( + inst._nextState, + type.getDerivedStateFromProps(newProps, inst._nextState) + ); + } else if ( + newProps !== prevProps && + inst.componentWillReceiveProps != null + ) { + inst.componentWillReceiveProps(newProps, componentContext); } - Object.assign( - c._nextState, - type.getDerivedStateFromProps(newProps, c._nextState) - ); - } + if ( + !(flags & FORCE_UPDATE) && + inst.shouldComponentUpdate != null && + inst.shouldComponentUpdate( + newProps, + inst._nextState, + componentContext + ) === false + ) { + inst.state = inst._nextState; + inst._nextState = null; + inst.props = newProps; - let oldProps = c.props; - let oldState = c.state; + // @TODO: should this really be flipped? + internal.flags &= ~DIRTY_BIT; + return; + } - if ( - type.getDerivedStateFromProps == null && - newProps !== oldProps && - c.componentWillReceiveProps != null - ) { - c.componentWillReceiveProps(newProps, componentContext); - } + if (inst.componentWillUpdate != null) { + inst.componentWillUpdate(newProps, inst._nextState, componentContext); + } - if ( - !(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; - } + inst.context = componentContext; + inst.props = newProps; + inst.state = inst._nextState; - if (c.componentWillUpdate != null) { - c.componentWillUpdate(newProps, c._nextState, componentContext); - } + let renderHook = options._render; + let renderResult; - c.context = componentContext; - internal.props = c.props = newProps; - c.state = c._nextState; - - let renderHook = options._render; - if (renderHook) renderHook(internal); - - let counter = 0, - renderResult; - while (counter++ < 25) { - internal.flags &= ~DIRTY_BIT; - if (renderHook) renderHook(internal); - if (internal.flags & TYPE_CLASS) { - renderResult = c.render(c.props, c.state, c.context); - // note: disable repeat render invocation for class components - break; + // note: disable repeat render invocation for class components + if (flags & TYPE_CLASS) { + internal.flags &= ~DIRTY_BIT; + if (renderHook) renderHook(internal); + renderResult = inst.render(inst.props, inst.state, inst.context); } else { - renderResult = type.call(c, c.props, c.context); - } - if (!(internal.flags & DIRTY_BIT)) { - break; + 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; + } + } } - } - - // Handle setState called in render, see #2553 - c.state = c._nextState; - if (c.getChildContext != null) { - rendererState._context = internal._context = Object.assign( - {}, - rendererState._context, - c.getChildContext() - ); - } + // Handle setState called in render, see #2553 + inst.state = inst._nextState; + // inst._nextState = null; - if (c.getSnapshotBeforeUpdate != null) { - snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState); - } + if (inst.getChildContext != null) { + internal._context = Object.assign({}, context, inst.getChildContext()); + } - // Only schedule componentDidUpdate if the component successfully rendered - if (c.componentDidUpdate != null) { - internal._commitCallbacks.push(() => { - c.componentDidUpdate(oldProps, oldState, snapshot); - }); - } + if (inst.getSnapshotBeforeUpdate != null) { + snapshot = inst.getSnapshotBeforeUpdate(prevProps, prevState); + } - if (renderResult == null) { - renderResult = []; - } else if (typeof renderResult === 'object') { - if (renderResult.type === Fragment && renderResult.key == null) { - renderResult = renderResult.props.children; + // Only schedule componentDidUpdate if the component successfully rendered + if (inst.componentDidUpdate != null) { + internal._commitCallbacks.push(() => { + inst.componentDidUpdate(prevProps, prevState, snapshot); + }); } - 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]; - } - return renderResult; + if (internal._children == null) { + let siblingDom; + if (flags & MODE_HYDRATE) { + siblingDom = flags & MODE_SUSPENDED ? internal._dom : null; + } else { + siblingDom = getDomSibling(internal); + } + + mountChildren(internal, renderResult, siblingDom); + } else { + patchChildren(internal, renderResult); + } + } 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); + } } 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..a6054c0649 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

@@ -110,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; @@ -124,6 +123,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. 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/_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/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); }); });