UNPKG

13.7 kBJavaScriptView Raw
1import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
2import { Component } from '../component';
3import { Fragment } from '../create-element';
4import { diffChildren, placeChild } from './children';
5import { diffProps, setProperty } from './props';
6import { assign, removeNode } from '../util';
7import options from '../options';
8
9function reorderChildren(newVNode, oldDom, parentDom) {
10 for (let tmp = 0; tmp < newVNode._children.length; tmp++) {
11 const vnode = newVNode._children[tmp];
12 if (vnode) {
13 vnode._parent = newVNode;
14
15 if (vnode._dom) {
16 if (typeof vnode.type == 'function' && vnode._children.length > 1) {
17 reorderChildren(vnode, oldDom, parentDom);
18 }
19
20 oldDom = placeChild(
21 parentDom,
22 vnode,
23 vnode,
24 newVNode._children,
25 null,
26 vnode._dom,
27 oldDom
28 );
29
30 if (typeof newVNode.type == 'function') {
31 newVNode._nextDom = oldDom;
32 }
33 }
34 }
35 }
36}
37
38/**
39 * Diff two virtual nodes and apply proper changes to the DOM
40 * @param {import('../internal').PreactElement} parentDom The parent of the DOM element
41 * @param {import('../internal').VNode} newVNode The new virtual node
42 * @param {import('../internal').VNode} oldVNode The old virtual node
43 * @param {object} globalContext The current context object. Modified by getChildContext
44 * @param {boolean} isSvg Whether or not this element is an SVG node
45 * @param {Array<import('../internal').PreactElement>} excessDomChildren
46 * @param {Array<import('../internal').Component>} commitQueue List of components
47 * which have callbacks to invoke in commitRoot
48 * @param {Element | Text} oldDom The current attached DOM
49 * element any new dom elements should be placed around. Likely `null` on first
50 * render (except when hydrating). Can be a sibling DOM element when diffing
51 * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
52 * @param {boolean} [isHydrating] Whether or not we are in hydration
53 */
54export function diff(
55 parentDom,
56 newVNode,
57 oldVNode,
58 globalContext,
59 isSvg,
60 excessDomChildren,
61 commitQueue,
62 oldDom,
63 isHydrating
64) {
65 let tmp,
66 newType = newVNode.type;
67
68 // When passing through createElement it assigns the object
69 // constructor as undefined. This to prevent JSON-injection.
70 if (newVNode.constructor !== undefined) return null;
71
72 if ((tmp = options._diff)) tmp(newVNode);
73
74 try {
75 outer: if (typeof newType == 'function') {
76 let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
77 let newProps = newVNode.props;
78
79 // Necessary for createContext api. Setting this property will pass
80 // the context value as `this.context` just for this component.
81 tmp = newType.contextType;
82 let provider = tmp && globalContext[tmp._id];
83 let componentContext = tmp
84 ? provider
85 ? provider.props.value
86 : tmp._defaultValue
87 : globalContext;
88
89 // Get component and set it to `c`
90 if (oldVNode._component) {
91 c = newVNode._component = oldVNode._component;
92 clearProcessingException = c._processingException = c._pendingError;
93 } else {
94 // Instantiate the new component
95 if ('prototype' in newType && newType.prototype.render) {
96 newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
97 } else {
98 newVNode._component = c = new Component(newProps, componentContext);
99 c.constructor = newType;
100 c.render = doRender;
101 }
102 if (provider) provider.sub(c);
103
104 c.props = newProps;
105 if (!c.state) c.state = {};
106 c.context = componentContext;
107 c._globalContext = globalContext;
108 isNew = c._dirty = true;
109 c._renderCallbacks = [];
110 }
111
112 // Invoke getDerivedStateFromProps
113 if (c._nextState == null) {
114 c._nextState = c.state;
115 }
116 if (newType.getDerivedStateFromProps != null) {
117 if (c._nextState == c.state) {
118 c._nextState = assign({}, c._nextState);
119 }
120
121 assign(
122 c._nextState,
123 newType.getDerivedStateFromProps(newProps, c._nextState)
124 );
125 }
126
127 oldProps = c.props;
128 oldState = c.state;
129
130 // Invoke pre-render lifecycle methods
131 if (isNew) {
132 if (
133 newType.getDerivedStateFromProps == null &&
134 c.componentWillMount != null
135 ) {
136 c.componentWillMount();
137 }
138
139 if (c.componentDidMount != null) {
140 c._renderCallbacks.push(c.componentDidMount);
141 }
142 } else {
143 if (
144 newType.getDerivedStateFromProps == null &&
145 newProps !== oldProps &&
146 c.componentWillReceiveProps != null
147 ) {
148 c.componentWillReceiveProps(newProps, componentContext);
149 }
150
151 if (
152 (!c._force &&
153 c.shouldComponentUpdate != null &&
154 c.shouldComponentUpdate(
155 newProps,
156 c._nextState,
157 componentContext
158 ) === false) ||
159 newVNode._original === oldVNode._original
160 ) {
161 c.props = newProps;
162 c.state = c._nextState;
163 // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
164 if (newVNode._original !== oldVNode._original) c._dirty = false;
165 c._vnode = newVNode;
166 newVNode._dom = oldVNode._dom;
167 newVNode._children = oldVNode._children;
168 if (c._renderCallbacks.length) {
169 commitQueue.push(c);
170 }
171
172 reorderChildren(newVNode, oldDom, parentDom);
173 break outer;
174 }
175
176 if (c.componentWillUpdate != null) {
177 c.componentWillUpdate(newProps, c._nextState, componentContext);
178 }
179
180 if (c.componentDidUpdate != null) {
181 c._renderCallbacks.push(() => {
182 c.componentDidUpdate(oldProps, oldState, snapshot);
183 });
184 }
185 }
186
187 c.context = componentContext;
188 c.props = newProps;
189 c.state = c._nextState;
190
191 if ((tmp = options._render)) tmp(newVNode);
192
193 c._dirty = false;
194 c._vnode = newVNode;
195 c._parentDom = parentDom;
196
197 tmp = c.render(c.props, c.state, c.context);
198
199 // Handle setState called in render, see #2553
200 c.state = c._nextState;
201
202 if (c.getChildContext != null) {
203 globalContext = assign(assign({}, globalContext), c.getChildContext());
204 }
205
206 if (!isNew && c.getSnapshotBeforeUpdate != null) {
207 snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
208 }
209
210 let isTopLevelFragment =
211 tmp != null && tmp.type == Fragment && tmp.key == null;
212 let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
213
214 diffChildren(
215 parentDom,
216 Array.isArray(renderResult) ? renderResult : [renderResult],
217 newVNode,
218 oldVNode,
219 globalContext,
220 isSvg,
221 excessDomChildren,
222 commitQueue,
223 oldDom,
224 isHydrating
225 );
226
227 c.base = newVNode._dom;
228
229 if (c._renderCallbacks.length) {
230 commitQueue.push(c);
231 }
232
233 if (clearProcessingException) {
234 c._pendingError = c._processingException = null;
235 }
236
237 c._force = false;
238 } else if (
239 excessDomChildren == null &&
240 newVNode._original === oldVNode._original
241 ) {
242 newVNode._children = oldVNode._children;
243 newVNode._dom = oldVNode._dom;
244 } else {
245 newVNode._dom = diffElementNodes(
246 oldVNode._dom,
247 newVNode,
248 oldVNode,
249 globalContext,
250 isSvg,
251 excessDomChildren,
252 commitQueue,
253 isHydrating
254 );
255 }
256
257 if ((tmp = options.diffed)) tmp(newVNode);
258 } catch (e) {
259 newVNode._original = null;
260 options._catchError(e, newVNode, oldVNode);
261 }
262
263 return newVNode._dom;
264}
265
266/**
267 * @param {Array<import('../internal').Component>} commitQueue List of components
268 * which have callbacks to invoke in commitRoot
269 * @param {import('../internal').VNode} root
270 */
271export function commitRoot(commitQueue, root) {
272 if (options._commit) options._commit(root, commitQueue);
273
274 commitQueue.some(c => {
275 try {
276 commitQueue = c._renderCallbacks;
277 c._renderCallbacks = [];
278 commitQueue.some(cb => {
279 cb.call(c);
280 });
281 } catch (e) {
282 options._catchError(e, c._vnode);
283 }
284 });
285}
286
287/**
288 * Diff two virtual nodes representing DOM element
289 * @param {import('../internal').PreactElement} dom The DOM element representing
290 * the virtual nodes being diffed
291 * @param {import('../internal').VNode} newVNode The new virtual node
292 * @param {import('../internal').VNode} oldVNode The old virtual node
293 * @param {object} globalContext The current context object
294 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
295 * @param {*} excessDomChildren
296 * @param {Array<import('../internal').Component>} commitQueue List of components
297 * which have callbacks to invoke in commitRoot
298 * @param {boolean} isHydrating Whether or not we are in hydration
299 * @returns {import('../internal').PreactElement}
300 */
301function diffElementNodes(
302 dom,
303 newVNode,
304 oldVNode,
305 globalContext,
306 isSvg,
307 excessDomChildren,
308 commitQueue,
309 isHydrating
310) {
311 let i;
312 let oldProps = oldVNode.props;
313 let newProps = newVNode.props;
314
315 // Tracks entering and exiting SVG namespace when descending through the tree.
316 isSvg = newVNode.type === 'svg' || isSvg;
317
318 if (excessDomChildren != null) {
319 for (i = 0; i < excessDomChildren.length; i++) {
320 const child = excessDomChildren[i];
321
322 // if newVNode matches an element in excessDomChildren or the `dom`
323 // argument matches an element in excessDomChildren, remove it from
324 // excessDomChildren so it isn't later removed in diffChildren
325 if (
326 child != null &&
327 ((newVNode.type === null
328 ? child.nodeType === 3
329 : child.localName === newVNode.type) ||
330 dom == child)
331 ) {
332 dom = child;
333 excessDomChildren[i] = null;
334 break;
335 }
336 }
337 }
338
339 if (dom == null) {
340 if (newVNode.type === null) {
341 return document.createTextNode(newProps);
342 }
343
344 dom = isSvg
345 ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
346 : document.createElement(
347 newVNode.type,
348 newProps.is && { is: newProps.is }
349 );
350 // we created a new parent, so none of the previously attached children can be reused:
351 excessDomChildren = null;
352 // we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate
353 isHydrating = false;
354 }
355
356 if (newVNode.type === null) {
357 if (oldProps !== newProps && dom.data !== newProps) {
358 dom.data = newProps;
359 }
360 } else {
361 if (excessDomChildren != null) {
362 excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
363 }
364
365 oldProps = oldVNode.props || EMPTY_OBJ;
366
367 let oldHtml = oldProps.dangerouslySetInnerHTML;
368 let newHtml = newProps.dangerouslySetInnerHTML;
369
370 // During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
371 // @TODO we should warn in debug mode when props don't match here.
372 if (!isHydrating) {
373 // But, if we are in a situation where we are using existing DOM (e.g. replaceNode)
374 // we should read the existing DOM attributes to diff them
375 if (excessDomChildren != null) {
376 oldProps = {};
377 for (let i = 0; i < dom.attributes.length; i++) {
378 oldProps[dom.attributes[i].name] = dom.attributes[i].value;
379 }
380 }
381
382 if (newHtml || oldHtml) {
383 // Avoid re-applying the same '__html' if it did not changed between re-render
384 if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
385 dom.innerHTML = (newHtml && newHtml.__html) || '';
386 }
387 }
388 }
389
390 diffProps(dom, newProps, oldProps, isSvg, isHydrating);
391
392 // If the new vnode didn't have dangerouslySetInnerHTML, diff its children
393 if (newHtml) {
394 newVNode._children = [];
395 } else {
396 i = newVNode.props.children;
397 diffChildren(
398 dom,
399 Array.isArray(i) ? i : [i],
400 newVNode,
401 oldVNode,
402 globalContext,
403 newVNode.type === 'foreignObject' ? false : isSvg,
404 excessDomChildren,
405 commitQueue,
406 EMPTY_OBJ,
407 isHydrating
408 );
409 }
410
411 // (as above, don't diff props during hydration)
412 if (!isHydrating) {
413 if (
414 'value' in newProps &&
415 (i = newProps.value) !== undefined &&
416 i !== dom.value
417 ) {
418 setProperty(dom, 'value', i, oldProps.value, false);
419 }
420 if (
421 'checked' in newProps &&
422 (i = newProps.checked) !== undefined &&
423 i !== dom.checked
424 ) {
425 setProperty(dom, 'checked', i, oldProps.checked, false);
426 }
427 }
428 }
429
430 return dom;
431}
432
433/**
434 * Invoke or update a ref, depending on whether it is a function or object ref.
435 * @param {object|function} ref
436 * @param {any} value
437 * @param {import('../internal').VNode} vnode
438 */
439export function applyRef(ref, value, vnode) {
440 try {
441 if (typeof ref == 'function') ref(value);
442 else ref.current = value;
443 } catch (e) {
444 options._catchError(e, vnode);
445 }
446}
447
448/**
449 * Unmount a virtual node from the tree and apply DOM changes
450 * @param {import('../internal').VNode} vnode The virtual node to unmount
451 * @param {import('../internal').VNode} parentVNode The parent of the VNode that
452 * initiated the unmount
453 * @param {boolean} [skipRemove] Flag that indicates that a parent node of the
454 * current element is already detached from the DOM.
455 */
456export function unmount(vnode, parentVNode, skipRemove) {
457 let r;
458 if (options.unmount) options.unmount(vnode);
459
460 if ((r = vnode.ref)) {
461 if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode);
462 }
463
464 let dom;
465 if (!skipRemove && typeof vnode.type != 'function') {
466 skipRemove = (dom = vnode._dom) != null;
467 }
468
469 // Must be set to `undefined` to properly clean up `_nextDom`
470 // for which `null` is a valid value. See comment in `create-element.js`
471 vnode._dom = vnode._nextDom = undefined;
472
473 if ((r = vnode._component) != null) {
474 if (r.componentWillUnmount) {
475 try {
476 r.componentWillUnmount();
477 } catch (e) {
478 options._catchError(e, parentVNode);
479 }
480 }
481
482 r.base = r._parentDom = null;
483 }
484
485 if ((r = vnode._children)) {
486 for (let i = 0; i < r.length; i++) {
487 if (r[i]) unmount(r[i], parentVNode, skipRemove);
488 }
489 }
490
491 if (dom != null) removeNode(dom);
492}
493
494/** The `.render()` method for a PFC backing instance. */
495function doRender(props, state, context) {
496 return this.constructor(props, context);
497}