UNPKG

10 kBJavaScriptView Raw
1import { diff, unmount, applyRef } from './index';
2import { createVNode, Fragment } from '../create-element';
3import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
4import { removeNode } from '../util';
5import { 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 */
27export 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 */
237export 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
250export 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}