1 | import { diff, unmount, applyRef } from './index';
|
2 | import { createVNode, Fragment } from '../create-element';
|
3 | import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
|
4 | import { removeNode } from '../util';
|
5 | import { getDomSibling } from '../component';
|
6 |
|
7 | /**
|
8 | * Diff the children of a virtual node
|
9 | * @param {import('../internal').PreactElement} parentDom The DOM element whose
|
10 | * children are being diffed
|
11 | * @param {import('../index').ComponentChildren[]} renderResult
|
12 | * @param {import('../internal').VNode} newParentVNode The new virtual
|
13 | * node whose children should be diff'ed against oldParentVNode
|
14 | * @param {import('../internal').VNode} oldParentVNode The old virtual
|
15 | * node whose children should be diff'ed against newParentVNode
|
16 | * @param {object} globalContext The current context object - modified by getChildContext
|
17 | * @param {boolean} isSvg Whether or not this DOM node is an SVG node
|
18 | * @param {Array<import('../internal').PreactElement>} excessDomChildren
|
19 | * @param {Array<import('../internal').Component>} commitQueue List of components
|
20 | * which have callbacks to invoke in commitRoot
|
21 | * @param {Node | Text} oldDom The current attached DOM
|
22 | * element any new dom elements should be placed around. Likely `null` on first
|
23 | * render (except when hydrating). Can be a sibling DOM element when diffing
|
24 | * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
|
25 | * @param {boolean} isHydrating Whether or not we are in hydration
|
26 | */
|
27 | export function diffChildren(
|
28 | parentDom,
|
29 | renderResult,
|
30 | newParentVNode,
|
31 | oldParentVNode,
|
32 | globalContext,
|
33 | isSvg,
|
34 | excessDomChildren,
|
35 | commitQueue,
|
36 | oldDom,
|
37 | isHydrating
|
38 | ) {
|
39 | let i, j, oldVNode, childVNode, newDom, firstChildDom, refs;
|
40 |
|
41 | // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR
|
42 | // as EMPTY_OBJ._children should be `undefined`.
|
43 | let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
|
44 |
|
45 | let oldChildrenLength = oldChildren.length;
|
46 |
|
47 | // Only in very specific places should this logic be invoked (top level `render` and `diffElementNodes`).
|
48 | // I'm using `EMPTY_OBJ` to signal when `diffChildren` is invoked in these situations. I can't use `null`
|
49 | // for this purpose, because `null` is a valid value for `oldDom` which can mean to skip to this logic
|
50 | // (e.g. if mounting a new tree in which the old DOM should be ignored (usually for Fragments).
|
51 | if (oldDom == EMPTY_OBJ) {
|
52 | if (excessDomChildren != null) {
|
53 | oldDom = excessDomChildren[0];
|
54 | } else if (oldChildrenLength) {
|
55 | oldDom = getDomSibling(oldParentVNode, 0);
|
56 | } else {
|
57 | oldDom = null;
|
58 | }
|
59 | }
|
60 |
|
61 | newParentVNode._children = [];
|
62 | for (i = 0; i < renderResult.length; i++) {
|
63 | childVNode = renderResult[i];
|
64 |
|
65 | if (childVNode == null || typeof childVNode == 'boolean') {
|
66 | childVNode = newParentVNode._children[i] = null;
|
67 | }
|
68 | // If this newVNode is being reused (e.g. <div>{reuse}{reuse}</div>) in the same diff,
|
69 | // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have
|
70 | // it's own DOM & etc. pointers
|
71 | else if (typeof childVNode == 'string' || typeof childVNode == 'number') {
|
72 | childVNode = newParentVNode._children[i] = createVNode(
|
73 | null,
|
74 | childVNode,
|
75 | null,
|
76 | null,
|
77 | childVNode
|
78 | );
|
79 | } else if (Array.isArray(childVNode)) {
|
80 | childVNode = newParentVNode._children[i] = createVNode(
|
81 | Fragment,
|
82 | { children: childVNode },
|
83 | null,
|
84 | null,
|
85 | null
|
86 | );
|
87 | } else if (childVNode._dom != null || childVNode._component != null) {
|
88 | childVNode = newParentVNode._children[i] = createVNode(
|
89 | childVNode.type,
|
90 | childVNode.props,
|
91 | childVNode.key,
|
92 | null,
|
93 | childVNode._original
|
94 | );
|
95 | } else {
|
96 | childVNode = newParentVNode._children[i] = childVNode;
|
97 | }
|
98 |
|
99 | // Terser removes the `continue` here and wraps the loop body
|
100 | // in a `if (childVNode) { ... } condition
|
101 | if (childVNode == null) {
|
102 | continue;
|
103 | }
|
104 |
|
105 | childVNode._parent = newParentVNode;
|
106 | childVNode._depth = newParentVNode._depth + 1;
|
107 |
|
108 | // Check if we find a corresponding element in oldChildren.
|
109 | // If found, delete the array item by setting to `undefined`.
|
110 | // We use `undefined`, as `null` is reserved for empty placeholders
|
111 | // (holes).
|
112 | oldVNode = oldChildren[i];
|
113 |
|
114 | if (
|
115 | oldVNode === null ||
|
116 | (oldVNode &&
|
117 | childVNode.key == oldVNode.key &&
|
118 | childVNode.type === oldVNode.type)
|
119 | ) {
|
120 | oldChildren[i] = undefined;
|
121 | } else {
|
122 | // Either oldVNode === undefined or oldChildrenLength > 0,
|
123 | // so after this loop oldVNode == null or oldVNode is a valid value.
|
124 | for (j = 0; j < oldChildrenLength; j++) {
|
125 | oldVNode = oldChildren[j];
|
126 | // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key.
|
127 | // We always match by type (in either case).
|
128 | if (
|
129 | oldVNode &&
|
130 | childVNode.key == oldVNode.key &&
|
131 | childVNode.type === oldVNode.type
|
132 | ) {
|
133 | oldChildren[j] = undefined;
|
134 | break;
|
135 | }
|
136 | oldVNode = null;
|
137 | }
|
138 | }
|
139 |
|
140 | oldVNode = oldVNode || EMPTY_OBJ;
|
141 |
|
142 | // Morph the old element into the new one, but don't append it to the dom yet
|
143 | newDom = diff(
|
144 | parentDom,
|
145 | childVNode,
|
146 | oldVNode,
|
147 | globalContext,
|
148 | isSvg,
|
149 | excessDomChildren,
|
150 | commitQueue,
|
151 | oldDom,
|
152 | isHydrating
|
153 | );
|
154 |
|
155 | if ((j = childVNode.ref) && oldVNode.ref != j) {
|
156 | if (!refs) refs = [];
|
157 | if (oldVNode.ref) refs.push(oldVNode.ref, null, childVNode);
|
158 | refs.push(j, childVNode._component || newDom, childVNode);
|
159 | }
|
160 |
|
161 | if (newDom != null) {
|
162 | if (firstChildDom == null) {
|
163 | firstChildDom = newDom;
|
164 | }
|
165 |
|
166 | oldDom = placeChild(
|
167 | parentDom,
|
168 | childVNode,
|
169 | oldVNode,
|
170 | oldChildren,
|
171 | excessDomChildren,
|
172 | newDom,
|
173 | oldDom
|
174 | );
|
175 |
|
176 | // Browsers will infer an option's `value` from `textContent` when
|
177 | // no value is present. This essentially bypasses our code to set it
|
178 | // later in `diff()`. It works fine in all browsers except for IE11
|
179 | // where it breaks setting `select.value`. There it will be always set
|
180 | // to an empty string. Re-applying an options value will fix that, so
|
181 | // there are probably some internal data structures that aren't
|
182 | // updated properly.
|
183 | //
|
184 | // To fix it we make sure to reset the inferred value, so that our own
|
185 | // value check in `diff()` won't be skipped.
|
186 | if (!isHydrating && newParentVNode.type == 'option') {
|
187 | parentDom.value = '';
|
188 | } else if (typeof newParentVNode.type == 'function') {
|
189 | // Because the newParentVNode is Fragment-like, we need to set it's
|
190 | // _nextDom property to the nextSibling of its last child DOM node.
|
191 | //
|
192 | // `oldDom` contains the correct value here because if the last child
|
193 | // is a Fragment-like, then oldDom has already been set to that child's _nextDom.
|
194 | // If the last child is a DOM VNode, then oldDom will be set to that DOM
|
195 | // node's nextSibling.
|
196 | newParentVNode._nextDom = oldDom;
|
197 | }
|
198 | } else if (
|
199 | oldDom &&
|
200 | oldVNode._dom == oldDom &&
|
201 | oldDom.parentNode != parentDom
|
202 | ) {
|
203 | // The above condition is to handle null placeholders. See test in placeholder.test.js:
|
204 | // `efficiently replace null placeholders in parent rerenders`
|
205 | oldDom = getDomSibling(oldVNode);
|
206 | }
|
207 | }
|
208 |
|
209 | newParentVNode._dom = firstChildDom;
|
210 |
|
211 | // Remove children that are not part of any vnode.
|
212 | if (excessDomChildren != null && typeof newParentVNode.type != 'function') {
|
213 | for (i = excessDomChildren.length; i--; ) {
|
214 | if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
|
215 | }
|
216 | }
|
217 |
|
218 | // Remove remaining oldChildren if there are any.
|
219 | for (i = oldChildrenLength; i--; ) {
|
220 | if (oldChildren[i] != null) unmount(oldChildren[i], oldChildren[i]);
|
221 | }
|
222 |
|
223 | // Set refs only after unmount
|
224 | if (refs) {
|
225 | for (i = 0; i < refs.length; i++) {
|
226 | applyRef(refs[i], refs[++i], refs[++i]);
|
227 | }
|
228 | }
|
229 | }
|
230 |
|
231 | /**
|
232 | * Flatten and loop through the children of a virtual node
|
233 | * @param {import('../index').ComponentChildren} children The unflattened
|
234 | * children of a virtual node
|
235 | * @returns {import('../internal').VNode[]}
|
236 | */
|
237 | export function toChildArray(children, out) {
|
238 | out = out || [];
|
239 | if (children == null || typeof children == 'boolean') {
|
240 | } else if (Array.isArray(children)) {
|
241 | children.some(child => {
|
242 | toChildArray(child, out);
|
243 | });
|
244 | } else {
|
245 | out.push(children);
|
246 | }
|
247 | return out;
|
248 | }
|
249 |
|
250 | export function placeChild(
|
251 | parentDom,
|
252 | childVNode,
|
253 | oldVNode,
|
254 | oldChildren,
|
255 | excessDomChildren,
|
256 | newDom,
|
257 | oldDom
|
258 | ) {
|
259 | let nextDom;
|
260 | if (childVNode._nextDom !== undefined) {
|
261 | // Only Fragments or components that return Fragment like VNodes will
|
262 | // have a non-undefined _nextDom. Continue the diff from the sibling
|
263 | // of last DOM child of this child VNode
|
264 | nextDom = childVNode._nextDom;
|
265 |
|
266 | // Eagerly cleanup _nextDom. We don't need to persist the value because
|
267 | // it is only used by `diffChildren` to determine where to resume the diff after
|
268 | // diffing Components and Fragments. Once we store it the nextDOM local var, we
|
269 | // can clean up the property
|
270 | childVNode._nextDom = undefined;
|
271 | } else if (
|
272 | excessDomChildren == oldVNode ||
|
273 | newDom != oldDom ||
|
274 | newDom.parentNode == null
|
275 | ) {
|
276 | // NOTE: excessDomChildren==oldVNode above:
|
277 | // This is a compression of excessDomChildren==null && oldVNode==null!
|
278 | // The values only have the same type when `null`.
|
279 |
|
280 | outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
|
281 | parentDom.appendChild(newDom);
|
282 | nextDom = null;
|
283 | } else {
|
284 | // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2`
|
285 | for (
|
286 | let sibDom = oldDom, j = 0;
|
287 | (sibDom = sibDom.nextSibling) && j < oldChildren.length;
|
288 | j += 2
|
289 | ) {
|
290 | if (sibDom == newDom) {
|
291 | break outer;
|
292 | }
|
293 | }
|
294 | parentDom.insertBefore(newDom, oldDom);
|
295 | nextDom = oldDom;
|
296 | }
|
297 | }
|
298 |
|
299 | // If we have pre-calculated the nextDOM node, use it. Else calculate it now
|
300 | // Strictly check for `undefined` here cuz `null` is a valid value of `nextDom`.
|
301 | // See more detail in create-element.js:createVNode
|
302 | if (nextDom !== undefined) {
|
303 | oldDom = nextDom;
|
304 | } else {
|
305 | oldDom = newDom.nextSibling;
|
306 | }
|
307 |
|
308 | return oldDom;
|
309 | }
|