diff options
Diffstat (limited to 'preact/src/diff')
-rw-r--r-- | preact/src/diff/catch-error.js | 38 | ||||
-rw-r--r-- | preact/src/diff/children.js | 347 | ||||
-rw-r--r-- | preact/src/diff/index.js | 514 | ||||
-rw-r--r-- | preact/src/diff/props.js | 158 |
4 files changed, 1057 insertions, 0 deletions
diff --git a/preact/src/diff/catch-error.js b/preact/src/diff/catch-error.js new file mode 100644 index 0000000..893a076 --- /dev/null +++ b/preact/src/diff/catch-error.js @@ -0,0 +1,38 @@ +/** + * Find the closest error boundary to a thrown error and call it + * @param {object} error The thrown value + * @param {import('../internal').VNode} vnode The vnode that threw + * the error that was caught (except for unmounting when this parameter + * is the highest parent that was being unmounted) + */ +export function _catchError(error, vnode) { + /** @type {import('../internal').Component} */ + let component, ctor, handled; + + for (; (vnode = vnode._parent); ) { + if ((component = vnode._component) && !component._processingException) { + try { + ctor = component.constructor; + + if (ctor && ctor.getDerivedStateFromError != null) { + component.setState(ctor.getDerivedStateFromError(error)); + handled = component._dirty; + } + + if (component.componentDidCatch != null) { + component.componentDidCatch(error); + handled = component._dirty; + } + + // This is an error boundary. Mark it as having bailed out, and whether it was mid-hydration. + if (handled) { + return (component._pendingError = component); + } + } catch (e) { + error = e; + } + } + } + + throw error; +} diff --git a/preact/src/diff/children.js b/preact/src/diff/children.js new file mode 100644 index 0000000..204e3e4 --- /dev/null +++ b/preact/src/diff/children.js @@ -0,0 +1,347 @@ +import { diff, unmount, applyRef } from './index'; +import { createVNode, Fragment } from '../create-element'; +import { EMPTY_OBJ, EMPTY_ARR } from '../constants'; +import { getDomSibling } from '../component'; + +/** + * Diff the children of a virtual node + * @param {import('../internal').PreactElement} parentDom The DOM element whose + * children are being diffed + * @param {import('../internal').ComponentChildren[]} renderResult + * @param {import('../internal').VNode} newParentVNode The new virtual + * node whose children should be diff'ed against oldParentVNode + * @param {import('../internal').VNode} oldParentVNode The old virtual + * node whose children should be diff'ed against newParentVNode + * @param {object} globalContext The current context object - modified by getChildContext + * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {Array<import('../internal').PreactElement>} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').PreactElement} oldDom The current attached DOM + * element any new dom elements should be placed around. Likely `null` on first + * render (except when hydrating). Can be a sibling DOM element when diffing + * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. + * @param {boolean} isHydrating Whether or not we are in hydration + */ +export function diffChildren( + parentDom, + renderResult, + newParentVNode, + oldParentVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating +) { + let i, j, oldVNode, childVNode, newDom, firstChildDom, refs; + + // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR + // as EMPTY_OBJ._children should be `undefined`. + let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; + + let oldChildrenLength = oldChildren.length; + + newParentVNode._children = []; + for (i = 0; i < renderResult.length; i++) { + childVNode = renderResult[i]; + + if (childVNode == null || typeof childVNode == 'boolean') { + childVNode = newParentVNode._children[i] = null; + } + // If this newVNode is being reused (e.g. <div>{reuse}{reuse}</div>) in the same diff, + // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have + // it's own DOM & etc. pointers + else if ( + typeof childVNode == 'string' || + typeof childVNode == 'number' || + // eslint-disable-next-line valid-typeof + typeof childVNode == 'bigint' + ) { + childVNode = newParentVNode._children[i] = createVNode( + null, + childVNode, + null, + null, + childVNode + ); + } else if (Array.isArray(childVNode)) { + childVNode = newParentVNode._children[i] = createVNode( + Fragment, + { children: childVNode }, + null, + null, + null + ); + } else if (childVNode._depth > 0) { + // VNode is already in use, clone it. This can happen in the following + // scenario: + // const reuse = <div /> + // <div>{reuse}<span />{reuse}</div> + childVNode = newParentVNode._children[i] = createVNode( + childVNode.type, + childVNode.props, + childVNode.key, + null, + childVNode._original + ); + } else { + childVNode = newParentVNode._children[i] = childVNode; + } + + // Terser removes the `continue` here and wraps the loop body + // in a `if (childVNode) { ... } condition + if (childVNode == null) { + continue; + } + + childVNode._parent = newParentVNode; + childVNode._depth = newParentVNode._depth + 1; + + // Check if we find a corresponding element in oldChildren. + // If found, delete the array item by setting to `undefined`. + // We use `undefined`, as `null` is reserved for empty placeholders + // (holes). + oldVNode = oldChildren[i]; + + if ( + oldVNode === null || + (oldVNode && + childVNode.key == oldVNode.key && + childVNode.type === oldVNode.type) + ) { + oldChildren[i] = undefined; + } else { + // Either oldVNode === undefined or oldChildrenLength > 0, + // so after this loop oldVNode == null or oldVNode is a valid value. + for (j = 0; j < oldChildrenLength; j++) { + oldVNode = oldChildren[j]; + // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key. + // We always match by type (in either case). + if ( + oldVNode && + childVNode.key == oldVNode.key && + childVNode.type === oldVNode.type + ) { + oldChildren[j] = undefined; + break; + } + oldVNode = null; + } + } + + oldVNode = oldVNode || EMPTY_OBJ; + + // Morph the old element into the new one, but don't append it to the dom yet + diff( + parentDom, + childVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating + ); + + newDom = childVNode._dom; + + if ((j = childVNode.ref) && oldVNode.ref != j) { + if (!refs) refs = []; + if (oldVNode.ref) refs.push(oldVNode.ref, null, childVNode); + refs.push(j, childVNode._component || newDom, childVNode); + } + + if (newDom != null) { + if (firstChildDom == null) { + firstChildDom = newDom; + } + + if ( + typeof childVNode.type == 'function' && + childVNode._children != null && // Can be null if childVNode suspended + childVNode._children === oldVNode._children + ) { + childVNode._nextDom = oldDom = reorderChildren( + childVNode, + oldDom, + parentDom + ); + } else { + oldDom = placeChild( + parentDom, + childVNode, + oldVNode, + oldChildren, + newDom, + oldDom + ); + } + + // Browsers will infer an option's `value` from `textContent` when + // no value is present. This essentially bypasses our code to set it + // later in `diff()`. It works fine in all browsers except for IE11 + // where it breaks setting `select.value`. There it will be always set + // to an empty string. Re-applying an options value will fix that, so + // there are probably some internal data structures that aren't + // updated properly. + // + // To fix it we make sure to reset the inferred value, so that our own + // value check in `diff()` won't be skipped. + if (!isHydrating && newParentVNode.type === 'option') { + // @ts-ignore We have validated that the type of parentDOM is 'option' + // in the above check + parentDom.value = ''; + } else if (typeof newParentVNode.type == 'function') { + // Because the newParentVNode is Fragment-like, we need to set it's + // _nextDom property to the nextSibling of its last child DOM node. + // + // `oldDom` contains the correct value here because if the last child + // is a Fragment-like, then oldDom has already been set to that child's _nextDom. + // If the last child is a DOM VNode, then oldDom will be set to that DOM + // node's nextSibling. + newParentVNode._nextDom = oldDom; + } + } else if ( + oldDom && + oldVNode._dom == oldDom && + oldDom.parentNode != parentDom + ) { + // The above condition is to handle null placeholders. See test in placeholder.test.js: + // `efficiently replace null placeholders in parent rerenders` + oldDom = getDomSibling(oldVNode); + } + } + + newParentVNode._dom = firstChildDom; + + // Remove remaining oldChildren if there are any. + for (i = oldChildrenLength; i--; ) { + if (oldChildren[i] != null) { + if ( + typeof newParentVNode.type == 'function' && + oldChildren[i]._dom != null && + oldChildren[i]._dom == newParentVNode._nextDom + ) { + // If the newParentVNode.__nextDom points to a dom node that is about to + // be unmounted, then get the next sibling of that vnode and set + // _nextDom to it + newParentVNode._nextDom = getDomSibling(oldParentVNode, i + 1); + } + + unmount(oldChildren[i], oldChildren[i]); + } + } + + // Set refs only after unmount + if (refs) { + for (i = 0; i < refs.length; i++) { + applyRef(refs[i], refs[++i], refs[++i]); + } + } +} + +function reorderChildren(childVNode, oldDom, parentDom) { + for (let tmp = 0; tmp < childVNode._children.length; tmp++) { + let vnode = childVNode._children[tmp]; + if (vnode) { + // We typically enter this code path on sCU bailout, where we copy + // oldVNode._children to newVNode._children. If that is the case, we need + // to update the old children's _parent pointer to point to the newVNode + // (childVNode here). + vnode._parent = childVNode; + + if (typeof vnode.type == 'function') { + oldDom = reorderChildren(vnode, oldDom, parentDom); + } else { + oldDom = placeChild( + parentDom, + vnode, + vnode, + childVNode._children, + vnode._dom, + oldDom + ); + } + } + } + + return oldDom; +} + +/** + * Flatten and loop through the children of a virtual node + * @param {import('../index').ComponentChildren} children The unflattened + * children of a virtual node + * @returns {import('../internal').VNode[]} + */ +export function toChildArray(children, out) { + out = out || []; + if (children == null || typeof children == 'boolean') { + } else if (Array.isArray(children)) { + children.some(child => { + toChildArray(child, out); + }); + } else { + out.push(children); + } + return out; +} + +function placeChild( + parentDom, + childVNode, + oldVNode, + oldChildren, + newDom, + oldDom +) { + let nextDom; + if (childVNode._nextDom !== undefined) { + // Only Fragments or components that return Fragment like VNodes will + // have a non-undefined _nextDom. Continue the diff from the sibling + // of last DOM child of this child VNode + nextDom = childVNode._nextDom; + + // Eagerly cleanup _nextDom. We don't need to persist the value because + // it is only used by `diffChildren` to determine where to resume the diff after + // diffing Components and Fragments. Once we store it the nextDOM local var, we + // can clean up the property + childVNode._nextDom = undefined; + } else if ( + oldVNode == null || + newDom != oldDom || + newDom.parentNode == null + ) { + outer: if (oldDom == null || oldDom.parentNode !== parentDom) { + parentDom.appendChild(newDom); + nextDom = null; + } else { + // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2` + for ( + let sibDom = oldDom, j = 0; + (sibDom = sibDom.nextSibling) && j < oldChildren.length; + j += 2 + ) { + if (sibDom == newDom) { + break outer; + } + } + parentDom.insertBefore(newDom, oldDom); + nextDom = oldDom; + } + } + + // If we have pre-calculated the nextDOM node, use it. Else calculate it now + // Strictly check for `undefined` here cuz `null` is a valid value of `nextDom`. + // See more detail in create-element.js:createVNode + if (nextDom !== undefined) { + oldDom = nextDom; + } else { + oldDom = newDom.nextSibling; + } + + return oldDom; +} diff --git a/preact/src/diff/index.js b/preact/src/diff/index.js new file mode 100644 index 0000000..de63843 --- /dev/null +++ b/preact/src/diff/index.js @@ -0,0 +1,514 @@ +import { EMPTY_OBJ } from '../constants'; +import { Component, getDomSibling } from '../component'; +import { Fragment } from '../create-element'; +import { diffChildren } from './children'; +import { diffProps, setProperty } from './props'; +import { assign, removeNode, slice } from '../util'; +import options from '../options'; + +/** + * Diff two virtual nodes and apply proper changes to the DOM + * @param {import('../internal').PreactElement} parentDom The parent of the DOM element + * @param {import('../internal').VNode} newVNode The new virtual node + * @param {import('../internal').VNode} oldVNode The old virtual node + * @param {object} globalContext The current context object. Modified by getChildContext + * @param {boolean} isSvg Whether or not this element is an SVG node + * @param {Array<import('../internal').PreactElement>} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').PreactElement} oldDom The current attached DOM + * element any new dom elements should be placed around. Likely `null` on first + * render (except when hydrating). Can be a sibling DOM element when diffing + * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. + * @param {boolean} [isHydrating] Whether or not we are in hydration + */ +export function diff( + parentDom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating +) { + let tmp, + newType = newVNode.type; + + // When passing through createElement it assigns the object + // constructor as undefined. This to prevent JSON-injection. + if (newVNode.constructor !== undefined) return null; + + // If the previous diff bailed out, resume creating/hydrating. + if (oldVNode._hydrating != null) { + isHydrating = oldVNode._hydrating; + oldDom = newVNode._dom = oldVNode._dom; + // if we resume, we want the tree to be "unlocked" + newVNode._hydrating = null; + excessDomChildren = [oldDom]; + } + + if ((tmp = options._diff)) tmp(newVNode); + + try { + outer: if (typeof newType == 'function') { + let c, isNew, oldProps, oldState, snapshot, clearProcessingException; + let newProps = newVNode.props; + + // Necessary for createContext api. Setting this property will pass + // the context value as `this.context` just for this component. + tmp = newType.contextType; + let provider = tmp && globalContext[tmp._id]; + let componentContext = tmp + ? provider + ? provider.props.value + : tmp._defaultValue + : globalContext; + + // Get component and set it to `c` + if (oldVNode._component) { + c = newVNode._component = oldVNode._component; + clearProcessingException = c._processingException = c._pendingError; + } else { + // Instantiate the new component + if ('prototype' in newType && newType.prototype.render) { + // @ts-ignore The check above verifies that newType is suppose to be constructed + newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap + } else { + // @ts-ignore Trust me, Component implements the interface we want + newVNode._component = c = new Component(newProps, componentContext); + c.constructor = newType; + c.render = doRender; + } + if (provider) provider.sub(c); + + c.props = newProps; + if (!c.state) c.state = {}; + c.context = componentContext; + c._globalContext = globalContext; + isNew = c._dirty = true; + c._renderCallbacks = []; + } + + // Invoke getDerivedStateFromProps + if (c._nextState == null) { + c._nextState = c.state; + } + if (newType.getDerivedStateFromProps != null) { + if (c._nextState == c.state) { + c._nextState = assign({}, c._nextState); + } + + assign( + c._nextState, + newType.getDerivedStateFromProps(newProps, c._nextState) + ); + } + + oldProps = c.props; + oldState = c.state; + + // Invoke pre-render lifecycle methods + if (isNew) { + if ( + newType.getDerivedStateFromProps == null && + c.componentWillMount != null + ) { + c.componentWillMount(); + } + + if (c.componentDidMount != null) { + c._renderCallbacks.push(c.componentDidMount); + } + } else { + if ( + newType.getDerivedStateFromProps == null && + newProps !== oldProps && + c.componentWillReceiveProps != null + ) { + c.componentWillReceiveProps(newProps, componentContext); + } + + if ( + (!c._force && + c.shouldComponentUpdate != null && + c.shouldComponentUpdate( + newProps, + c._nextState, + componentContext + ) === false) || + newVNode._original === oldVNode._original + ) { + c.props = newProps; + c.state = c._nextState; + // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8 + if (newVNode._original !== oldVNode._original) c._dirty = false; + c._vnode = newVNode; + newVNode._dom = oldVNode._dom; + newVNode._children = oldVNode._children; + newVNode._children.forEach(vnode => { + if (vnode) vnode._parent = newVNode; + }); + if (c._renderCallbacks.length) { + commitQueue.push(c); + } + + break outer; + } + + if (c.componentWillUpdate != null) { + c.componentWillUpdate(newProps, c._nextState, componentContext); + } + + if (c.componentDidUpdate != null) { + c._renderCallbacks.push(() => { + c.componentDidUpdate(oldProps, oldState, snapshot); + }); + } + } + + c.context = componentContext; + c.props = newProps; + c.state = c._nextState; + + if ((tmp = options._render)) tmp(newVNode); + + c._dirty = false; + c._vnode = newVNode; + c._parentDom = parentDom; + + tmp = c.render(c.props, c.state, c.context); + + // Handle setState called in render, see #2553 + c.state = c._nextState; + + if (c.getChildContext != null) { + globalContext = assign(assign({}, globalContext), c.getChildContext()); + } + + if (!isNew && c.getSnapshotBeforeUpdate != null) { + snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState); + } + + let isTopLevelFragment = + tmp != null && tmp.type === Fragment && tmp.key == null; + let renderResult = isTopLevelFragment ? tmp.props.children : tmp; + + diffChildren( + parentDom, + Array.isArray(renderResult) ? renderResult : [renderResult], + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating + ); + + c.base = newVNode._dom; + + // We successfully rendered this VNode, unset any stored hydration/bailout state: + newVNode._hydrating = null; + + if (c._renderCallbacks.length) { + commitQueue.push(c); + } + + if (clearProcessingException) { + c._pendingError = c._processingException = null; + } + + c._force = false; + } else if ( + excessDomChildren == null && + newVNode._original === oldVNode._original + ) { + newVNode._children = oldVNode._children; + newVNode._dom = oldVNode._dom; + } else { + newVNode._dom = diffElementNodes( + oldVNode._dom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + isHydrating + ); + } + + if ((tmp = options.diffed)) tmp(newVNode); + } catch (e) { + newVNode._original = null; + // if hydrating or creating initial tree, bailout preserves DOM: + if (isHydrating || excessDomChildren != null) { + newVNode._dom = oldDom; + newVNode._hydrating = !!isHydrating; + excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; + // ^ could possibly be simplified to: + // excessDomChildren.length = 0; + } + options._catchError(e, newVNode, oldVNode); + } +} + +/** + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').VNode} root + */ +export function commitRoot(commitQueue, root) { + if (options._commit) options._commit(root, commitQueue); + + commitQueue.some(c => { + try { + // @ts-ignore Reuse the commitQueue variable here so the type changes + commitQueue = c._renderCallbacks; + c._renderCallbacks = []; + commitQueue.some(cb => { + // @ts-ignore See above ts-ignore on commitQueue + cb.call(c); + }); + } catch (e) { + options._catchError(e, c._vnode); + } + }); +} + +/** + * Diff two virtual nodes representing DOM element + * @param {import('../internal').PreactElement} dom The DOM element representing + * the virtual nodes being diffed + * @param {import('../internal').VNode} newVNode The new virtual node + * @param {import('../internal').VNode} oldVNode The old virtual node + * @param {object} globalContext The current context object + * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {*} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {boolean} isHydrating Whether or not we are in hydration + * @returns {import('../internal').PreactElement} + */ +function diffElementNodes( + dom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + isHydrating +) { + let oldProps = oldVNode.props; + let newProps = newVNode.props; + let nodeType = newVNode.type; + let i = 0; + + // Tracks entering and exiting SVG namespace when descending through the tree. + if (nodeType === 'svg') isSvg = true; + + if (excessDomChildren != null) { + for (; i < excessDomChildren.length; i++) { + const child = excessDomChildren[i]; + + // if newVNode matches an element in excessDomChildren or the `dom` + // argument matches an element in excessDomChildren, remove it from + // excessDomChildren so it isn't later removed in diffChildren + if ( + child && + (child === dom || + (nodeType ? child.localName == nodeType : child.nodeType == 3)) + ) { + dom = child; + excessDomChildren[i] = null; + break; + } + } + } + + if (dom == null) { + if (nodeType === null) { + // @ts-ignore createTextNode returns Text, we expect PreactElement + return document.createTextNode(newProps); + } + + 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 created a new parent, so none of the previously attached children can be reused: + excessDomChildren = null; + // we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate + isHydrating = false; + } + + if (nodeType === null) { + // During hydration, we still have to split merged text from SSR'd HTML. + if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) { + dom.data = newProps; + } + } else { + // If excessDomChildren was not null, repopulate it with the current element's children: + excessDomChildren = excessDomChildren && slice.call(dom.childNodes); + + oldProps = oldVNode.props || EMPTY_OBJ; + + let oldHtml = oldProps.dangerouslySetInnerHTML; + let newHtml = newProps.dangerouslySetInnerHTML; + + // During hydration, props are not diffed at all (including dangerouslySetInnerHTML) + // @TODO we should warn in debug mode when props don't match here. + if (!isHydrating) { + // 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 + if (excessDomChildren != null) { + oldProps = {}; + for (i = 0; i < dom.attributes.length; i++) { + oldProps[dom.attributes[i].name] = dom.attributes[i].value; + } + } + + if (newHtml || oldHtml) { + // Avoid re-applying the same '__html' if it did not changed between re-render + if ( + !newHtml || + ((!oldHtml || newHtml.__html != oldHtml.__html) && + newHtml.__html !== dom.innerHTML) + ) { + dom.innerHTML = (newHtml && newHtml.__html) || ''; + } + } + } + + diffProps(dom, newProps, oldProps, isSvg, isHydrating); + + // If the new vnode didn't have dangerouslySetInnerHTML, diff its children + if (newHtml) { + newVNode._children = []; + } else { + i = newVNode.props.children; + diffChildren( + dom, + Array.isArray(i) ? i : [i], + newVNode, + oldVNode, + globalContext, + isSvg && nodeType !== 'foreignObject', + excessDomChildren, + commitQueue, + excessDomChildren + ? excessDomChildren[0] + : oldVNode._children && getDomSibling(oldVNode, 0), + isHydrating + ); + + // Remove children that are not part of any vnode. + if (excessDomChildren != null) { + for (i = excessDomChildren.length; i--; ) { + if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]); + } + } + } + + // (as above, don't diff props during hydration) + if (!isHydrating) { + if ( + 'value' in newProps && + (i = newProps.value) !== undefined && + // #2756 For the <progress>-element the initial value is 0, + // despite the attribute not being present. When the attribute + // is missing the progress bar is treated as indeterminate. + // To fix that we'll always update it when it is 0 for progress elements + (i !== dom.value || (nodeType === 'progress' && !i)) + ) { + setProperty(dom, 'value', i, oldProps.value, false); + } + if ( + 'checked' in newProps && + (i = newProps.checked) !== undefined && + i !== dom.checked + ) { + setProperty(dom, 'checked', i, oldProps.checked, false); + } + } + } + + return dom; +} + +/** + * Invoke or update a ref, depending on whether it is a function or object ref. + * @param {object|function} ref + * @param {any} value + * @param {import('../internal').VNode} vnode + */ +export function applyRef(ref, value, vnode) { + try { + if (typeof ref == 'function') ref(value); + else ref.current = value; + } catch (e) { + options._catchError(e, vnode); + } +} + +/** + * Unmount a virtual node from the tree and apply DOM changes + * @param {import('../internal').VNode} vnode The virtual node to unmount + * @param {import('../internal').VNode} parentVNode The parent of the VNode that + * initiated the unmount + * @param {boolean} [skipRemove] Flag that indicates that a parent node of the + * current element is already detached from the DOM. + */ +export function unmount(vnode, parentVNode, skipRemove) { + let r; + if (options.unmount) options.unmount(vnode); + + if ((r = vnode.ref)) { + if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode); + } + + if ((r = vnode._component) != null) { + if (r.componentWillUnmount) { + try { + r.componentWillUnmount(); + } catch (e) { + options._catchError(e, parentVNode); + } + } + + r.base = r._parentDom = null; + } + + if ((r = vnode._children)) { + for (let i = 0; i < r.length; i++) { + if (r[i]) { + unmount(r[i], parentVNode, typeof vnode.type != 'function'); + } + } + } + + if (!skipRemove && vnode._dom != null) removeNode(vnode._dom); + + // Must be set to `undefined` to properly clean up `_nextDom` + // for which `null` is a valid value. See comment in `create-element.js` + vnode._dom = vnode._nextDom = undefined; +} + +/** The `.render()` method for a PFC backing instance. */ +function doRender(props, state, context) { + return this.constructor(props, context); +} diff --git a/preact/src/diff/props.js b/preact/src/diff/props.js new file mode 100644 index 0000000..472d997 --- /dev/null +++ b/preact/src/diff/props.js @@ -0,0 +1,158 @@ +import { IS_NON_DIMENSIONAL } from '../constants'; +import options from '../options'; + +/** + * Diff the old and new properties of a VNode and apply changes to the DOM node + * @param {import('../internal').PreactElement} dom The DOM node to apply + * changes to + * @param {object} newProps The new props + * @param {object} oldProps The old props + * @param {boolean} isSvg Whether or not this node is an SVG node + * @param {boolean} hydrate Whether or not we are in hydration mode + */ +export function diffProps(dom, newProps, oldProps, isSvg, hydrate) { + let i; + + for (i in oldProps) { + if (i !== 'children' && i !== 'key' && !(i in newProps)) { + setProperty(dom, i, null, oldProps[i], isSvg); + } + } + + for (i in newProps) { + if ( + (!hydrate || typeof newProps[i] == 'function') && + i !== 'children' && + i !== 'key' && + i !== 'value' && + i !== 'checked' && + oldProps[i] !== newProps[i] + ) { + setProperty(dom, i, newProps[i], oldProps[i], isSvg); + } + } +} + +function setStyle(style, key, value) { + if (key[0] === '-') { + style.setProperty(key, value); + } else if (value == null) { + style[key] = ''; + } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) { + style[key] = value; + } else { + style[key] = value + 'px'; + } +} + +/** + * Set a property value on a DOM node + * @param {import('../internal').PreactElement} dom The DOM node to modify + * @param {string} name The name of the property to set + * @param {*} value The value to set the property to + * @param {*} oldValue The old value the property had + * @param {boolean} isSvg Whether or not this DOM node is an SVG node or not + */ +export function setProperty(dom, name, value, oldValue, isSvg) { + let useCapture; + + o: if (name === 'style') { + if (typeof value == 'string') { + dom.style.cssText = value; + } else { + if (typeof oldValue == 'string') { + dom.style.cssText = oldValue = ''; + } + + if (oldValue) { + for (name in oldValue) { + if (!(value && name in value)) { + setStyle(dom.style, name, ''); + } + } + } + + if (value) { + for (name in value) { + if (!oldValue || value[name] !== oldValue[name]) { + setStyle(dom.style, name, value[name]); + } + } + } + } + } + // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6 + else if (name[0] === 'o' && name[1] === 'n') { + useCapture = name !== (name = name.replace(/Capture$/, '')); + + // Infer correct casing for DOM built-in events: + if (name.toLowerCase() in dom) name = name.toLowerCase().slice(2); + else name = name.slice(2); + + if (!dom._listeners) dom._listeners = {}; + dom._listeners[name + useCapture] = value; + + if (value) { + if (!oldValue) { + const handler = useCapture ? eventProxyCapture : eventProxy; + dom.addEventListener(name, handler, useCapture); + } + } else { + const handler = useCapture ? eventProxyCapture : eventProxy; + dom.removeEventListener(name, handler, useCapture); + } + } else if (name !== 'dangerouslySetInnerHTML') { + if (isSvg) { + // Normalize incorrect prop usage for SVG: + // - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed) + // - className --> class + name = name.replace(/xlink[H:h]/, 'h').replace(/sName$/, 's'); + } else if ( + name !== 'href' && + name !== 'list' && + name !== 'form' && + // Default value in browsers is `-1` and an empty string is + // cast to `0` instead + name !== 'tabIndex' && + name !== 'download' && + name in dom + ) { + try { + dom[name] = value == null ? '' : value; + // labelled break is 1b smaller here than a return statement (sorry) + break o; + } catch (e) {} + } + + // ARIA-attributes have a different notion of boolean values. + // The value `false` is different from the attribute not + // existing on the DOM, so we can't remove it. For non-boolean + // ARIA-attributes we could treat false as a removal, but the + // amount of exceptions would cost us too many bytes. On top of + // that other VDOM frameworks also always stringify `false`. + + if (typeof value === 'function') { + // never serialize functions as attribute values + } else if ( + value != null && + (value !== false || (name[0] === 'a' && name[1] === 'r')) + ) { + dom.setAttribute(name, value); + } else { + dom.removeAttribute(name); + } + } +} + +/** + * Proxy an event to hooked event handlers + * @param {Event} e The event object from the browser + * @private + */ +function eventProxy(e) { + this._listeners[e.type + false](options.event ? options.event(e) : e); +} + +function eventProxyCapture(e) { + this._listeners[e.type + true](options.event ? options.event(e) : e); +} |