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