UNPKG

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