preact
Version:
Fast 3kb React-compatible Virtual DOM library.
470 lines (412 loc) • 13 kB
JavaScript
import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
import { Component } from '../component';
import { Fragment } from '../create-element';
import { diffChildren } from './children';
import { diffProps, setProperty } from './props';
import { assign, removeNode } 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 {Element | Text} 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 ((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) {
newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
} else {
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._processingException)
) {
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;
if (c._renderCallbacks.length) {
commitQueue.push(c);
}
for (tmp = 0; tmp < newVNode._children.length; tmp++) {
if (newVNode._children[tmp]) {
newVNode._children[tmp]._parent = newVNode;
}
}
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);
let isTopLevelFragment =
tmp != null && tmp.type == Fragment && tmp.key == null;
newVNode._children = isTopLevelFragment
? tmp.props.children
: Array.isArray(tmp)
? tmp
: [tmp];
if (c.getChildContext != null) {
globalContext = assign(assign({}, globalContext), c.getChildContext());
}
if (!isNew && c.getSnapshotBeforeUpdate != null) {
snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}
diffChildren(
parentDom,
newVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating
);
c.base = newVNode._dom;
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;
options._catchError(e, newVNode, oldVNode);
}
return newVNode._dom;
}
/**
* @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 {
commitQueue = c._renderCallbacks;
c._renderCallbacks = [];
commitQueue.some(cb => {
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 i;
let oldProps = oldVNode.props;
let newProps = newVNode.props;
// Tracks entering and exiting SVG namespace when descending through the tree.
isSvg = newVNode.type === 'svg' || isSvg;
if (excessDomChildren != null) {
for (i = 0; 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 != null &&
((newVNode.type === null
? child.nodeType === 3
: child.localName === newVNode.type) ||
dom == child)
) {
dom = child;
excessDomChildren[i] = null;
break;
}
}
}
if (dom == null) {
if (newVNode.type === null) {
return document.createTextNode(newProps);
}
dom = isSvg
? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
: document.createElement(
newVNode.type,
newProps.is && { is: newProps.is }
);
// 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 (newVNode.type === null) {
if (oldProps !== newProps && dom.data != newProps) {
dom.data = newProps;
}
} else {
if (excessDomChildren != null) {
excessDomChildren = EMPTY_ARR.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) {
if (oldProps === EMPTY_OBJ) {
oldProps = {};
for (let 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) {
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 {
newVNode._children = newVNode.props.children;
diffChildren(
dom,
newVNode,
oldVNode,
globalContext,
newVNode.type === 'foreignObject' ? false : isSvg,
excessDomChildren,
commitQueue,
EMPTY_OBJ,
isHydrating
);
}
// (as above, don't diff props during hydration)
if (!isHydrating) {
if (
'value' in newProps &&
(i = newProps.value) !== undefined &&
i !== dom.value
) {
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);
}
let dom;
if (!skipRemove && typeof vnode.type != 'function') {
skipRemove = (dom = vnode._dom) != null;
}
// 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;
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, skipRemove);
}
}
if (dom != null) removeNode(dom);
}
/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
return this.constructor(props, context);
}