-
-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathcomponent.js
254 lines (225 loc) · 7.71 KB
/
component.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import { assign } from './util';
import { diff, commitRoot } from './diff/index';
import options from './options';
import { Fragment } from './create-element';
import { EMPTY_ARR, MODE_HYDRATE } from './constants';
/**
* 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 BaseComponent(props, context) {
this.props = props;
this.context = context;
}
/**
* Update component state and schedule a re-render.
* @this {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
*/
BaseComponent.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._stateCallbacks.push(callback);
}
enqueueRender(this);
}
};
/**
* Immediately perform a synchronous re-render of the component
* @this {Component}
* @param {() => void} [callback] A function to be called after component is
* re-rendered
*/
BaseComponent.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 {ComponentChildren | void}
*/
BaseComponent.prototype.render = Fragment;
/**
* @param {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._index + 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 {Component} component The component to rerender
*/
function renderComponent(component, commitQueue, refQueue) {
let oldVNode = component._vnode,
oldDom = oldVNode._dom,
parentDom = component._parentDom;
if (parentDom) {
const newVNode = assign({}, oldVNode);
newVNode._original = oldVNode._original + 1;
if (options.vnode) options.vnode(newVNode);
diff(
parentDom,
newVNode,
oldVNode,
component._globalContext,
parentDom.ownerSVGElement !== undefined,
oldVNode._flags & MODE_HYDRATE ? [oldDom] : null,
commitQueue,
oldDom == null ? getDomSibling(oldVNode) : oldDom,
!!(oldVNode._flags & MODE_HYDRATE),
refQueue
);
newVNode._parent._children[newVNode._index] = newVNode;
newVNode._nextDom = undefined;
if (newVNode._dom != oldDom) {
updateParentDomPointers(newVNode);
}
return newVNode;
}
}
/**
* @param {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<Component>}
*/
let rerenderQueue = [];
/*
* 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;
const defer =
typeof Promise == 'function'
? Promise.prototype.then.bind(Promise.resolve())
: setTimeout;
/**
* Enqueue a rerender of a component
* @param {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);
}
}
/**
* @param {Component} a
* @param {Component} b
*/
const depthSort = (a, b) => a._vnode._depth - b._vnode._depth;
/** Flush the render queue by rerendering all queued components */
function process() {
let c;
let commitQueue = [];
let refQueue = [];
let root;
rerenderQueue.sort(depthSort);
// Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
// process() calls from getting scheduled while `queue` is still being consumed.
while ((c = rerenderQueue.shift())) {
if (c._dirty) {
let renderQueueLength = rerenderQueue.length;
root = renderComponent(c, commitQueue, refQueue) || root;
// If this WAS the last component in the queue, run commit callbacks *before* we exit the tight loop.
// This is required in order for `componentDidMount(){this.setState()}` to be batched into one flush.
// Otherwise, also run commit callbacks if the render queue was mutated.
if (renderQueueLength === 0 || rerenderQueue.length > renderQueueLength) {
commitRoot(commitQueue, root, refQueue);
refQueue.length = commitQueue.length = 0;
root = undefined;
// When i.e. rerendering a provider additional new items can be injected, we want to
// keep the order from top to bottom with those new items so we can handle them in a
// single pass
rerenderQueue.sort(depthSort);
} else if (root) {
if (options._commit) options._commit(root, EMPTY_ARR);
}
}
}
if (root) commitRoot(commitQueue, root, refQueue);
process._rerenderCount = 0;
}
process._rerenderCount = 0;