UNPKG

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