UNPKG

6.83 kBJavaScriptView Raw
1import { assign } from './util';
2import { diff, commitRoot } from './diff/index';
3import options from './options';
4import { Fragment } from './create-element';
5
6/**
7 * Base Component class. Provides `setState()` and `forceUpdate()`, which
8 * trigger rendering
9 * @param {object} props The initial component props
10 * @param {object} context The initial context from parent components'
11 * getChildContext
12 */
13export function Component(props, context) {
14 this.props = props;
15 this.context = context;
16}
17
18/**
19 * Update component state and schedule a re-render.
20 * @param {object | ((s: object, p: object) => object)} update A hash of state
21 * properties to update with new values or a function that given the current
22 * state and props returns a new partial state
23 * @param {() => void} [callback] A function to be called once component state is
24 * updated
25 */
26Component.prototype.setState = function(update, callback) {
27 // only clone state when copying to nextState the first time.
28 let s;
29 if (this._nextState != null && this._nextState !== this.state) {
30 s = this._nextState;
31 } else {
32 s = this._nextState = assign({}, this.state);
33 }
34
35 if (typeof update == 'function') {
36 // Some libraries like `immer` mark the current state as readonly,
37 // preventing us from mutating it, so we need to clone it. See #2716
38 update = update(assign({}, s), this.props);
39 }
40
41 if (update) {
42 assign(s, update);
43 }
44
45 // Skip update if updater function returned null
46 if (update == null) return;
47
48 if (this._vnode) {
49 if (callback) this._renderCallbacks.push(callback);
50 enqueueRender(this);
51 }
52};
53
54/**
55 * Immediately perform a synchronous re-render of the component
56 * @param {() => void} [callback] A function to be called after component is
57 * re-rendered
58 */
59Component.prototype.forceUpdate = function(callback) {
60 if (this._vnode) {
61 // Set render mode so that we can differentiate where the render request
62 // is coming from. We need this because forceUpdate should never call
63 // shouldComponentUpdate
64 this._force = true;
65 if (callback) this._renderCallbacks.push(callback);
66 enqueueRender(this);
67 }
68};
69
70/**
71 * Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
72 * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
73 * @param {object} props Props (eg: JSX attributes) received from parent
74 * element/component
75 * @param {object} state The component's current state
76 * @param {object} context Context object, as returned by the nearest
77 * ancestor's `getChildContext()`
78 * @returns {import('./index').ComponentChildren | void}
79 */
80Component.prototype.render = Fragment;
81
82/**
83 * @param {import('./internal').VNode} vnode
84 * @param {number | null} [childIndex]
85 */
86export function getDomSibling(vnode, childIndex) {
87 if (childIndex == null) {
88 // Use childIndex==null as a signal to resume the search from the vnode's sibling
89 return vnode._parent
90 ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
91 : null;
92 }
93
94 let sibling;
95 for (; childIndex < vnode._children.length; childIndex++) {
96 sibling = vnode._children[childIndex];
97
98 if (sibling != null && sibling._dom != null) {
99 // Since updateParentDomPointers keeps _dom pointer correct,
100 // we can rely on _dom to tell us if this subtree contains a
101 // rendered DOM node, and what the first rendered DOM node is
102 return sibling._dom;
103 }
104 }
105
106 // If we get here, we have not found a DOM node in this vnode's children.
107 // We must resume from this vnode's sibling (in it's parent _children array)
108 // Only climb up and search the parent if we aren't searching through a DOM
109 // VNode (meaning we reached the DOM parent of the original vnode that began
110 // the search)
111 return typeof vnode.type == 'function' ? getDomSibling(vnode) : null;
112}
113
114/**
115 * Trigger in-place re-rendering of a component.
116 * @param {import('./internal').Component} component The component to rerender
117 */
118function renderComponent(component) {
119 let vnode = component._vnode,
120 oldDom = vnode._dom,
121 parentDom = component._parentDom;
122
123 if (parentDom) {
124 let commitQueue = [];
125 const oldVNode = assign({}, vnode);
126 oldVNode._original = oldVNode;
127
128 let newDom = diff(
129 parentDom,
130 vnode,
131 oldVNode,
132 component._globalContext,
133 parentDom.ownerSVGElement !== undefined,
134 null,
135 commitQueue,
136 oldDom == null ? getDomSibling(vnode) : oldDom
137 );
138 commitRoot(commitQueue, vnode);
139
140 if (newDom != oldDom) {
141 updateParentDomPointers(vnode);
142 }
143 }
144}
145
146/**
147 * @param {import('./internal').VNode} vnode
148 */
149function updateParentDomPointers(vnode) {
150 if ((vnode = vnode._parent) != null && vnode._component != null) {
151 vnode._dom = vnode._component.base = null;
152 for (let i = 0; i < vnode._children.length; i++) {
153 let child = vnode._children[i];
154 if (child != null && child._dom != null) {
155 vnode._dom = vnode._component.base = child._dom;
156 break;
157 }
158 }
159
160 return updateParentDomPointers(vnode);
161 }
162}
163
164/**
165 * The render queue
166 * @type {Array<import('./internal').Component>}
167 */
168let rerenderQueue = [];
169
170/**
171 * Asynchronously schedule a callback
172 * @type {(cb: () => void) => void}
173 */
174/* istanbul ignore next */
175// Note the following line isn't tree-shaken by rollup cuz of rollup/rollup#2566
176const defer =
177 typeof Promise == 'function'
178 ? Promise.prototype.then.bind(Promise.resolve())
179 : setTimeout;
180
181/*
182 * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is
183 * important that contributors to Preact can consistently reason about what calls to `setState`, etc.
184 * do, and when their effects will be applied. See the links below for some further reading on designing
185 * asynchronous APIs.
186 * * [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony)
187 * * [Callbacks synchronous and asynchronous](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/)
188 */
189
190let prevDebounce;
191
192/**
193 * Enqueue a rerender of a component
194 * @param {import('./internal').Component} c The component to rerender
195 */
196export function enqueueRender(c) {
197 if (
198 (!c._dirty &&
199 (c._dirty = true) &&
200 rerenderQueue.push(c) &&
201 !process._rerenderCount++) ||
202 prevDebounce !== options.debounceRendering
203 ) {
204 prevDebounce = options.debounceRendering;
205 (prevDebounce || defer)(process);
206 }
207}
208
209/** Flush the render queue by rerendering all queued components */
210function process() {
211 let queue;
212 while ((process._rerenderCount = rerenderQueue.length)) {
213 queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
214 rerenderQueue = [];
215 // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
216 // process() calls from getting scheduled while `queue` is still being consumed.
217 queue.some(c => {
218 if (c._dirty) renderComponent(c);
219 });
220 }
221}
222process._rerenderCount = 0;