diff options
Diffstat (limited to 'preact/src/component.js')
-rw-r--r-- | preact/src/component.js | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/preact/src/component.js b/preact/src/component.js new file mode 100644 index 0000000..c2043af --- /dev/null +++ b/preact/src/component.js @@ -0,0 +1,225 @@ +import { assign } from './util'; +import { diff, commitRoot } from './diff/index'; +import options from './options'; +import { Fragment } from './create-element'; + +/** + * Base Component class. Provides `setState()` and `forceUpdate()`, which + * trigger rendering + * @param {object} props The initial component props + * @param {object} context The initial context from parent components' + * getChildContext + */ +export function Component(props, context) { + this.props = props; + this.context = context; +} + +/** + * Update component state and schedule a re-render. + * @this {import('./internal').Component} + * @param {object | ((s: object, p: object) => object)} update A hash of state + * properties to update with new values or a function that given the current + * state and props returns a new partial state + * @param {() => void} [callback] A function to be called once component state is + * updated + */ +Component.prototype.setState = function(update, callback) { + // only clone state when copying to nextState the first time. + let s; + if (this._nextState != null && this._nextState !== this.state) { + s = this._nextState; + } else { + s = this._nextState = assign({}, this.state); + } + + if (typeof update == 'function') { + // Some libraries like `immer` mark the current state as readonly, + // preventing us from mutating it, so we need to clone it. See #2716 + update = update(assign({}, s), this.props); + } + + if (update) { + assign(s, update); + } + + // Skip update if updater function returned null + if (update == null) return; + + if (this._vnode) { + if (callback) this._renderCallbacks.push(callback); + enqueueRender(this); + } +}; + +/** + * Immediately perform a synchronous re-render of the component + * @this {import('./internal').Component} + * @param {() => void} [callback] A function to be called after component is + * re-rendered + */ +Component.prototype.forceUpdate = function(callback) { + if (this._vnode) { + // Set render mode so that we can differentiate where the render request + // is coming from. We need this because forceUpdate should never call + // shouldComponentUpdate + this._force = true; + if (callback) this._renderCallbacks.push(callback); + enqueueRender(this); + } +}; + +/** + * Accepts `props` and `state`, and returns a new Virtual DOM tree to build. + * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx). + * @param {object} props Props (eg: JSX attributes) received from parent + * element/component + * @param {object} state The component's current state + * @param {object} context Context object, as returned by the nearest + * ancestor's `getChildContext()` + * @returns {import('./index').ComponentChildren | void} + */ +Component.prototype.render = Fragment; + +/** + * @param {import('./internal').VNode} vnode + * @param {number | null} [childIndex] + */ +export function getDomSibling(vnode, childIndex) { + if (childIndex == null) { + // Use childIndex==null as a signal to resume the search from the vnode's sibling + return vnode._parent + ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1) + : null; + } + + let sibling; + for (; childIndex < vnode._children.length; childIndex++) { + sibling = vnode._children[childIndex]; + + if (sibling != null && sibling._dom != null) { + // Since updateParentDomPointers keeps _dom pointer correct, + // we can rely on _dom to tell us if this subtree contains a + // rendered DOM node, and what the first rendered DOM node is + return sibling._dom; + } + } + + // If we get here, we have not found a DOM node in this vnode's children. + // We must resume from this vnode's sibling (in it's parent _children array) + // Only climb up and search the parent if we aren't searching through a DOM + // VNode (meaning we reached the DOM parent of the original vnode that began + // the search) + return typeof vnode.type == 'function' ? getDomSibling(vnode) : null; +} + +/** + * Trigger in-place re-rendering of a component. + * @param {import('./internal').Component} component The component to rerender + */ +function renderComponent(component) { + let vnode = component._vnode, + oldDom = vnode._dom, + parentDom = component._parentDom; + + if (parentDom) { + let commitQueue = []; + const oldVNode = assign({}, vnode); + oldVNode._original = vnode._original + 1; + + diff( + parentDom, + vnode, + oldVNode, + component._globalContext, + parentDom.ownerSVGElement !== undefined, + vnode._hydrating != null ? [oldDom] : null, + commitQueue, + oldDom == null ? getDomSibling(vnode) : oldDom, + vnode._hydrating + ); + commitRoot(commitQueue, vnode); + + if (vnode._dom != oldDom) { + updateParentDomPointers(vnode); + } + } +} + +/** + * @param {import('./internal').VNode} vnode + */ +function updateParentDomPointers(vnode) { + if ((vnode = vnode._parent) != null && vnode._component != null) { + vnode._dom = vnode._component.base = null; + for (let i = 0; i < vnode._children.length; i++) { + let child = vnode._children[i]; + if (child != null && child._dom != null) { + vnode._dom = vnode._component.base = child._dom; + break; + } + } + + return updateParentDomPointers(vnode); + } +} + +/** + * The render queue + * @type {Array<import('./internal').Component>} + */ +let rerenderQueue = []; + +/** + * Asynchronously schedule a callback + * @type {(cb: () => void) => void} + */ +/* istanbul ignore next */ +// Note the following line isn't tree-shaken by rollup cuz of rollup/rollup#2566 +const defer = + typeof Promise == 'function' + ? Promise.prototype.then.bind(Promise.resolve()) + : setTimeout; + +/* + * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is + * important that contributors to Preact can consistently reason about what calls to `setState`, etc. + * do, and when their effects will be applied. See the links below for some further reading on designing + * asynchronous APIs. + * * [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony) + * * [Callbacks synchronous and asynchronous](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/) + */ + +let prevDebounce; + +/** + * Enqueue a rerender of a component + * @param {import('./internal').Component} c The component to rerender + */ +export function enqueueRender(c) { + if ( + (!c._dirty && + (c._dirty = true) && + rerenderQueue.push(c) && + !process._rerenderCount++) || + prevDebounce !== options.debounceRendering + ) { + prevDebounce = options.debounceRendering; + (prevDebounce || defer)(process); + } +} + +/** Flush the render queue by rerendering all queued components */ +function process() { + let queue; + while ((process._rerenderCount = rerenderQueue.length)) { + queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); + rerenderQueue = []; + // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary + // process() calls from getting scheduled while `queue` is still being consumed. + queue.some(c => { + if (c._dirty) renderComponent(c); + }); + } +} +process._rerenderCount = 0; |