1 | import { assign } from './util';
|
2 | import { diff, commitRoot } from './diff/index';
|
3 | import options from './options';
|
4 | import { 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 | */
|
13 | export 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 | */
|
27 | Component.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 | */
|
63 | Component.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 | */
|
84 | Component.prototype.render = Fragment;
|
85 |
|
86 | /**
|
87 | * @param {import('./internal').VNode} vnode
|
88 | * @param {number | null} [childIndex]
|
89 | */
|
90 | export 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 | */
|
122 | function 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 | */
|
154 | function 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 | */
|
173 | let 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 |
|
184 | let prevDebounce;
|
185 |
|
186 | /**
|
187 | * Enqueue a rerender of a component
|
188 | * @param {import('./internal').Component} c The component to rerender
|
189 | */
|
190 | export 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 */
|
204 | function 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 |
|
217 | process._rerenderCount = 0;
|