import { assign } from './util'; import { diff, commitRoot } from './diff/index'; import options from './options'; import { Fragment } from './create-element'; import { MODE_HYDRATE } from './constants'; /** * 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 BaseComponent(props, context) { this.props = props; this.context = context; } /** * Update component state and schedule a re-render. * @this {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 */ BaseComponent.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._stateCallbacks.push(callback); } enqueueRender(this); } }; /** * Immediately perform a synchronous re-render of the component * @this {Component} * @param {() => void} [callback] A function to be called after component is * re-rendered */ BaseComponent.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 {ComponentChildren | void} */ BaseComponent.prototype.render = Fragment; /** * @param {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._index + 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 {Component} component The component to rerender */ function renderComponent(component) { let oldVNode = component._vnode, oldDom = oldVNode._dom, parentDom = component._parentDom, commitQueue = [], refQueue = []; if (parentDom) { const newVNode = assign({}, oldVNode); newVNode._original = oldVNode._original + 1; if (options.vnode) options.vnode(newVNode); diff( parentDom, newVNode, oldVNode, component._globalContext, parentDom.ownerSVGElement !== undefined, oldVNode._flags & MODE_HYDRATE ? [oldDom] : null, commitQueue, oldDom == null ? getDomSibling(oldVNode) : oldDom, !!(oldVNode._flags & MODE_HYDRATE), refQueue ); newVNode._original = oldVNode._original; newVNode._parent._children[newVNode._index] = newVNode; commitRoot(commitQueue, newVNode, refQueue); if (newVNode._dom != oldDom) { updateParentDomPointers(newVNode); } } } /** * @param {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} */ let rerenderQueue = []; /* * 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; const defer = typeof Promise == 'function' ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout; /** * Enqueue a rerender of a component * @param {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); } } /** * @param {Component} a * @param {Component} b */ const depthSort = (a, b) => a._vnode._depth - b._vnode._depth; /** Flush the render queue by rerendering all queued components */ function process() { let c; rerenderQueue.sort(depthSort); // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary // process() calls from getting scheduled while `queue` is still being consumed. while ((c = rerenderQueue.shift())) { if (c._dirty) { let renderQueueLength = rerenderQueue.length; renderComponent(c); if (rerenderQueue.length > renderQueueLength) { // When i.e. rerendering a provider additional new items can be injected, we want to // keep the order from top to bottom with those new items so we can handle them in a // single pass rerenderQueue.sort(depthSort); } } } process._rerenderCount = 0; } process._rerenderCount = 0;