summaryrefslogtreecommitdiff
path: root/preact/src/component.js
diff options
context:
space:
mode:
Diffstat (limited to 'preact/src/component.js')
-rw-r--r--preact/src/component.js225
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;