diff options
Diffstat (limited to 'thirdparty/preact/src/vdom')
-rw-r--r-- | thirdparty/preact/src/vdom/component-recycler.js | 32 | ||||
-rw-r--r-- | thirdparty/preact/src/vdom/component.js | 281 | ||||
-rw-r--r-- | thirdparty/preact/src/vdom/diff.js | 254 | ||||
-rw-r--r-- | thirdparty/preact/src/vdom/functional-component.js | 25 | ||||
-rw-r--r-- | thirdparty/preact/src/vdom/index.js | 49 |
5 files changed, 641 insertions, 0 deletions
diff --git a/thirdparty/preact/src/vdom/component-recycler.js b/thirdparty/preact/src/vdom/component-recycler.js new file mode 100644 index 000000000..a70f0ece0 --- /dev/null +++ b/thirdparty/preact/src/vdom/component-recycler.js @@ -0,0 +1,32 @@ +import { Component } from '../component'; + +/** Retains a pool of Components for re-use, keyed on component name. + * Note: since component names are not unique or even necessarily available, these are primarily a form of sharding. + * @private + */ +const components = {}; + + +export function collectComponent(component) { + let name = component.constructor.name, + list = components[name]; + if (list) list.push(component); + else components[name] = [component]; +} + + +export function createComponent(Ctor, props, context) { + let inst = new Ctor(props, context), + list = components[Ctor.name]; + Component.call(inst, props, context); + if (list) { + for (let i=list.length; i--; ) { + if (list[i].constructor===Ctor) { + inst.nextBase = list[i].nextBase; + list.splice(i, 1); + break; + } + } + } + return inst; +} diff --git a/thirdparty/preact/src/vdom/component.js b/thirdparty/preact/src/vdom/component.js new file mode 100644 index 000000000..64e7ff81f --- /dev/null +++ b/thirdparty/preact/src/vdom/component.js @@ -0,0 +1,281 @@ +import { SYNC_RENDER, NO_RENDER, FORCE_RENDER, ASYNC_RENDER, ATTR_KEY } from '../constants'; +import options from '../options'; +import { isFunction, clone, extend } from '../util'; +import { enqueueRender } from '../render-queue'; +import { getNodeProps } from './index'; +import { diff, mounts, diffLevel, flushMounts, removeOrphanedChildren, recollectNodeTree } from './diff'; +import { isFunctionalComponent, buildFunctionalComponent } from './functional-component'; +import { createComponent, collectComponent } from './component-recycler'; +import { removeNode } from '../dom/index'; + + + +/** Set a component's `props` (generally derived from JSX attributes). + * @param {Object} props + * @param {Object} [opts] + * @param {boolean} [opts.renderSync=false] If `true` and {@link options.syncComponentUpdates} is `true`, triggers synchronous rendering. + * @param {boolean} [opts.render=true] If `false`, no render will be triggered. + */ +export function setComponentProps(component, props, opts, context, mountAll) { + if (component._disable) return; + component._disable = true; + + if ((component.__ref = props.ref)) delete props.ref; + if ((component.__key = props.key)) delete props.key; + + if (!component.base || mountAll) { + if (component.componentWillMount) component.componentWillMount(); + } + else if (component.componentWillReceiveProps) { + component.componentWillReceiveProps(props, context); + } + + if (context && context!==component.context) { + if (!component.prevContext) component.prevContext = component.context; + component.context = context; + } + + if (!component.prevProps) component.prevProps = component.props; + component.props = props; + + component._disable = false; + + if (opts!==NO_RENDER) { + if (opts===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) { + renderComponent(component, SYNC_RENDER, mountAll); + } + else { + enqueueRender(component); + } + } + + if (component.__ref) component.__ref(component); +} + + + +/** Render a Component, triggering necessary lifecycle events and taking High-Order Components into account. + * @param {Component} component + * @param {Object} [opts] + * @param {boolean} [opts.build=false] If `true`, component will build and store a DOM node if not already associated with one. + * @private + */ +export function renderComponent(component, opts, mountAll, isChild) { + if (component._disable) return; + + let skip, rendered, + props = component.props, + state = component.state, + context = component.context, + previousProps = component.prevProps || props, + previousState = component.prevState || state, + previousContext = component.prevContext || context, + isUpdate = component.base, + nextBase = component.nextBase, + initialBase = isUpdate || nextBase, + initialChildComponent = component._component, + inst, cbase; + + // if updating + if (isUpdate) { + component.props = previousProps; + component.state = previousState; + component.context = previousContext; + if (opts!==FORCE_RENDER + && component.shouldComponentUpdate + && component.shouldComponentUpdate(props, state, context) === false) { + skip = true; + } + else if (component.componentWillUpdate) { + component.componentWillUpdate(props, state, context); + } + component.props = props; + component.state = state; + component.context = context; + } + + component.prevProps = component.prevState = component.prevContext = component.nextBase = null; + component._dirty = false; + + if (!skip) { + if (component.render) rendered = component.render(props, state, context); + + // context to pass to the child, can be updated via (grand-)parent component + if (component.getChildContext) { + context = extend(clone(context), component.getChildContext()); + } + + while (isFunctionalComponent(rendered)) { + rendered = buildFunctionalComponent(rendered, context); + } + + let childComponent = rendered && rendered.nodeName, + toUnmount, base; + + if (isFunction(childComponent)) { + // set up high order component link + + + inst = initialChildComponent; + let childProps = getNodeProps(rendered); + + if (inst && inst.constructor===childComponent) { + setComponentProps(inst, childProps, SYNC_RENDER, context); + } + else { + toUnmount = inst; + + inst = createComponent(childComponent, childProps, context); + inst.nextBase = inst.nextBase || nextBase; + inst._parentComponent = component; + component._component = inst; + setComponentProps(inst, childProps, NO_RENDER, context); + renderComponent(inst, SYNC_RENDER, mountAll, true); + } + + base = inst.base; + } + else { + cbase = initialBase; + + // destroy high order component link + toUnmount = initialChildComponent; + if (toUnmount) { + cbase = component._component = null; + } + + if (initialBase || opts===SYNC_RENDER) { + if (cbase) cbase._component = null; + base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true); + } + } + + if (initialBase && base!==initialBase && inst!==initialChildComponent) { + let baseParent = initialBase.parentNode; + if (baseParent && base!==baseParent) { + baseParent.replaceChild(base, initialBase); + + if (!toUnmount) { + initialBase._component = null; + recollectNodeTree(initialBase); + } + } + } + + if (toUnmount) { + unmountComponent(toUnmount, base!==initialBase); + } + + component.base = base; + if (base && !isChild) { + let componentRef = component, + t = component; + while ((t=t._parentComponent)) { + (componentRef = t).base = base; + } + base._component = componentRef; + base._componentConstructor = componentRef.constructor; + } + } + + if (!isUpdate || mountAll) { + mounts.unshift(component); + } + else if (!skip) { + if (component.componentDidUpdate) { + component.componentDidUpdate(previousProps, previousState, previousContext); + } + if (options.afterUpdate) options.afterUpdate(component); + } + + let cb = component._renderCallbacks, fn; + if (cb) while ( (fn = cb.pop()) ) fn.call(component); + + if (!diffLevel && !isChild) flushMounts(); +} + + + +/** Apply the Component referenced by a VNode to the DOM. + * @param {Element} dom The DOM node to mutate + * @param {VNode} vnode A Component-referencing VNode + * @returns {Element} dom The created/mutated element + * @private + */ +export function buildComponentFromVNode(dom, vnode, context, mountAll) { + let c = dom && dom._component, + oldDom = dom, + isDirectOwner = c && dom._componentConstructor===vnode.nodeName, + isOwner = isDirectOwner, + props = getNodeProps(vnode); + while (c && !isOwner && (c=c._parentComponent)) { + isOwner = c.constructor===vnode.nodeName; + } + + if (c && isOwner && (!mountAll || c._component)) { + setComponentProps(c, props, ASYNC_RENDER, context, mountAll); + dom = c.base; + } + else { + if (c && !isDirectOwner) { + unmountComponent(c, true); + dom = oldDom = null; + } + + c = createComponent(vnode.nodeName, props, context); + if (dom && !c.nextBase) { + c.nextBase = dom; + // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L241: + oldDom = null; + } + setComponentProps(c, props, SYNC_RENDER, context, mountAll); + dom = c.base; + + if (oldDom && dom!==oldDom) { + oldDom._component = null; + recollectNodeTree(oldDom); + } + } + + return dom; +} + + + +/** Remove a component from the DOM and recycle it. + * @param {Element} dom A DOM node from which to unmount the given Component + * @param {Component} component The Component instance to unmount + * @private + */ +export function unmountComponent(component, remove) { + if (options.beforeUnmount) options.beforeUnmount(component); + + // console.log(`${remove?'Removing':'Unmounting'} component: ${component.constructor.name}`); + let base = component.base; + + component._disable = true; + + if (component.componentWillUnmount) component.componentWillUnmount(); + + component.base = null; + + // recursively tear down & recollect high-order component children: + let inner = component._component; + if (inner) { + unmountComponent(inner, remove); + } + else if (base) { + if (base[ATTR_KEY] && base[ATTR_KEY].ref) base[ATTR_KEY].ref(null); + + component.nextBase = base; + + if (remove) { + removeNode(base); + collectComponent(component); + } + removeOrphanedChildren(base.childNodes, !remove); + } + + if (component.__ref) component.__ref(null); + if (component.componentDidUnmount) component.componentDidUnmount(); +} diff --git a/thirdparty/preact/src/vdom/diff.js b/thirdparty/preact/src/vdom/diff.js new file mode 100644 index 000000000..794a79aaa --- /dev/null +++ b/thirdparty/preact/src/vdom/diff.js @@ -0,0 +1,254 @@ +import { ATTR_KEY } from '../constants'; +import { isString, isFunction } from '../util'; +import { isSameNodeType, isNamedNode } from './index'; +import { isFunctionalComponent, buildFunctionalComponent } from './functional-component'; +import { buildComponentFromVNode } from './component'; +import { setAccessor } from '../dom/index'; +import { createNode, collectNode } from '../dom/recycler'; +import { unmountComponent } from './component'; +import options from '../options'; + + +/** Diff recursion count, used to track the end of the diff cycle. */ +export const mounts = []; + +/** Diff recursion count, used to track the end of the diff cycle. */ +export let diffLevel = 0; + +let isSvgMode = false; + + +export function flushMounts() { + let c; + while ((c=mounts.pop())) { + if (options.afterMount) options.afterMount(c); + if (c.componentDidMount) c.componentDidMount(); + } +} + + +/** Apply differences in a given vnode (and it's deep children) to a real DOM Node. + * @param {Element} [dom=null] A DOM node to mutate into the shape of the `vnode` + * @param {VNode} vnode A VNode (with descendants forming a tree) representing the desired DOM structure + * @returns {Element} dom The created/mutated element + * @private + */ +export function diff(dom, vnode, context, mountAll, parent, componentRoot) { + if (!diffLevel++) isSvgMode = parent instanceof SVGElement; + let ret = idiff(dom, vnode, context, mountAll); + if (parent && ret.parentNode!==parent) parent.appendChild(ret); + if (!--diffLevel && !componentRoot) flushMounts(); + return ret; +} + + +function idiff(dom, vnode, context, mountAll) { + let originalAttributes = vnode && vnode.attributes; + + while (isFunctionalComponent(vnode)) { + vnode = buildFunctionalComponent(vnode, context); + } + + if (vnode==null) vnode = ''; + + if (isString(vnode)) { + if (dom) { + if (dom instanceof Text && dom.parentNode) { + if (dom.nodeValue!=vnode) { + dom.nodeValue = vnode; + } + return dom; + } + recollectNodeTree(dom); + } + return document.createTextNode(vnode); + } + + if (isFunction(vnode.nodeName)) { + return buildComponentFromVNode(dom, vnode, context, mountAll); + } + + let out = dom, + nodeName = vnode.nodeName, + prevSvgMode = isSvgMode, + vchildren = vnode.children; + + if (!isString(nodeName)) { + nodeName = String(nodeName); + } + + isSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode; + + if (!dom) { + out = createNode(nodeName, isSvgMode); + } + else if (!isNamedNode(dom, nodeName)) { + out = createNode(nodeName, isSvgMode); + // move children into the replacement node + while (dom.firstChild) out.appendChild(dom.firstChild); + // reclaim element nodes + recollectNodeTree(dom); + } + + // fast-path for elements containing a single TextNode: + if (vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) { + if (out.firstChild.nodeValue!=vchildren[0]) { + out.firstChild.nodeValue = vchildren[0]; + } + } + else if (vchildren && vchildren.length || out.firstChild) { + innerDiffNode(out, vchildren, context, mountAll); + } + + let props = out[ATTR_KEY]; + if (!props) { + out[ATTR_KEY] = props = {}; + for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value; + } + + diffAttributes(out, vnode.attributes, props); + + if (originalAttributes && typeof originalAttributes.ref==='function') { + (props.ref = originalAttributes.ref)(out); + } + + isSvgMode = prevSvgMode; + + return out; +} + + +/** Apply child and attribute changes between a VNode and a DOM Node to the DOM. */ +function innerDiffNode(dom, vchildren, context, mountAll) { + let originalChildren = dom.childNodes, + children = [], + keyed = {}, + keyedLen = 0, + min = 0, + len = originalChildren.length, + childrenLen = 0, + vlen = vchildren && vchildren.length, + j, c, vchild, child; + + if (len) { + for (let i=0; i<len; i++) { + let child = originalChildren[i], + key = vlen ? ((c = child._component) ? c.__key : (c = child[ATTR_KEY]) ? c.key : null) : null; + if (key || key===0) { + keyedLen++; + keyed[key] = child; + } + else { + children[childrenLen++] = child; + } + } + } + + if (vlen) { + for (let i=0; i<vlen; i++) { + vchild = vchildren[i]; + child = null; + + // if (isFunctionalComponent(vchild)) { + // vchild = buildFunctionalComponent(vchild); + // } + + // attempt to find a node based on key matching + let key = vchild.key; + if (key!=null) { + if (keyedLen && key in keyed) { + child = keyed[key]; + keyed[key] = undefined; + keyedLen--; + } + } + // attempt to pluck a node of the same type from the existing children + else if (!child && min<childrenLen) { + for (j=min; j<childrenLen; j++) { + c = children[j]; + if (c && isSameNodeType(c, vchild)) { + child = c; + children[j] = undefined; + if (j===childrenLen-1) childrenLen--; + if (j===min) min++; + break; + } + } + if (!child && min<childrenLen && isFunction(vchild.nodeName) && mountAll) { + child = children[min]; + children[min++] = undefined; + } + } + + // morph the matched/found/created DOM child to match vchild (deep) + child = idiff(child, vchild, context, mountAll); + + if (child && child!==dom && child!==originalChildren[i]) { + dom.insertBefore(child, originalChildren[i] || null); + } + } + } + + + if (keyedLen) { + for (let i in keyed) if (keyed[i]) recollectNodeTree(keyed[i]); + } + + // remove orphaned children + if (min<childrenLen) { + removeOrphanedChildren(children); + } +} + + +/** Reclaim children that were unreferenced in the desired VTree */ +export function removeOrphanedChildren(children, unmountOnly) { + for (let i=children.length; i--; ) { + if (children[i]) { + recollectNodeTree(children[i], unmountOnly); + } + } +} + + +/** Reclaim an entire tree of nodes, starting at the root. */ +export function recollectNodeTree(node, unmountOnly) { + // @TODO: Need to make a call on whether Preact should remove nodes not created by itself. + // Currently it *does* remove them. Discussion: https://github.com/developit/preact/issues/39 + //if (!node[ATTR_KEY]) return; + + let component = node._component; + if (component) { + unmountComponent(component, !unmountOnly); + } + else { + if (node[ATTR_KEY] && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null); + + if (!unmountOnly) { + collectNode(node); + } + + if (node.childNodes && node.childNodes.length) { + removeOrphanedChildren(node.childNodes, unmountOnly); + } + } +} + + +/** Apply differences in attributes from a VNode to the given DOM Node. */ +function diffAttributes(dom, attrs, old) { + for (let name in old) { + if (!(attrs && name in attrs) && old[name]!=null) { + setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode); + } + } + + // new & updated + if (attrs) { + for (let name in attrs) { + if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) { + setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode); + } + } + } +} diff --git a/thirdparty/preact/src/vdom/functional-component.js b/thirdparty/preact/src/vdom/functional-component.js new file mode 100644 index 000000000..04bc5a464 --- /dev/null +++ b/thirdparty/preact/src/vdom/functional-component.js @@ -0,0 +1,25 @@ +import { EMPTY } from '../constants'; +import { getNodeProps } from './index'; +import { isFunction } from '../util'; + + +/** Check if a VNode is a reference to a stateless functional component. + * A function component is represented as a VNode whose `nodeName` property is a reference to a function. + * If that function is not a Component (ie, has no `.render()` method on a prototype), it is considered a stateless functional component. + * @param {VNode} vnode A VNode + * @private + */ +export function isFunctionalComponent(vnode) { + let nodeName = vnode && vnode.nodeName; + return nodeName && isFunction(nodeName) && !(nodeName.prototype && nodeName.prototype.render); +} + + + +/** Construct a resultant VNode from a VNode referencing a stateless functional component. + * @param {VNode} vnode A VNode with a `nodeName` property that is a reference to a function. + * @private + */ +export function buildFunctionalComponent(vnode, context) { + return vnode.nodeName(getNodeProps(vnode), context || EMPTY); +} diff --git a/thirdparty/preact/src/vdom/index.js b/thirdparty/preact/src/vdom/index.js new file mode 100644 index 000000000..f59fbae21 --- /dev/null +++ b/thirdparty/preact/src/vdom/index.js @@ -0,0 +1,49 @@ +import { clone, isString, isFunction, toLowerCase } from '../util'; +import { isFunctionalComponent } from './functional-component'; + + +/** Check if two nodes are equivalent. + * @param {Element} node + * @param {VNode} vnode + * @private + */ +export function isSameNodeType(node, vnode) { + if (isString(vnode)) { + return node instanceof Text; + } + if (isString(vnode.nodeName)) { + return isNamedNode(node, vnode.nodeName); + } + if (isFunction(vnode.nodeName)) { + return node._componentConstructor===vnode.nodeName || isFunctionalComponent(vnode); + } +} + + +export function isNamedNode(node, nodeName) { + return node.normalizedNodeName===nodeName || toLowerCase(node.nodeName)===toLowerCase(nodeName); +} + + +/** + * Reconstruct Component-style `props` from a VNode. + * Ensures default/fallback values from `defaultProps`: + * Own-properties of `defaultProps` not present in `vnode.attributes` are added. + * @param {VNode} vnode + * @returns {Object} props + */ +export function getNodeProps(vnode) { + let props = clone(vnode.attributes); + props.children = vnode.children; + + let defaultProps = vnode.nodeName.defaultProps; + if (defaultProps) { + for (let i in defaultProps) { + if (props[i]===undefined) { + props[i] = defaultProps[i]; + } + } + } + + return props; +} |