UNPKG

9.9 kBJavaScriptView Raw
1import { ATTR_KEY } from '../constants';
2import { isSameNodeType, isNamedNode } from './index';
3import { buildComponentFromVNode } from './component';
4import { createNode, setAccessor } from '../dom/index';
5import { unmountComponent } from './component';
6import options from '../options';
7import { removeNode } from '../dom/index';
8
9/** Queue of components that have been mounted and are awaiting componentDidMount */
10export const mounts = [];
11
12/** Diff recursion count, used to track the end of the diff cycle. */
13export let diffLevel = 0;
14
15/** Global flag indicating if the diff is currently within an SVG */
16let isSvgMode = false;
17
18/** Global flag indicating if the diff is performing hydration */
19let hydrating = false;
20
21/** Invoke queued componentDidMount lifecycle methods */
22export function flushMounts() {
23 let c;
24 while ((c=mounts.pop())) {
25 if (options.afterMount) options.afterMount(c);
26 if (c.componentDidMount) c.componentDidMount();
27 }
28}
29
30
31/** Apply differences in a given vnode (and it's deep children) to a real DOM Node.
32 * @param {Element} [dom=null] A DOM node to mutate into the shape of the `vnode`
33 * @param {VNode} vnode A VNode (with descendants forming a tree) representing the desired DOM structure
34 * @returns {Element} dom The created/mutated element
35 * @private
36 */
37export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
38 // diffLevel having been 0 here indicates initial entry into the diff (not a subdiff)
39 if (!diffLevel++) {
40 // when first starting the diff, check if we're diffing an SVG or within an SVG
41 isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
42
43 // hydration is indicated by the existing element to be diffed not having a prop cache
44 hydrating = dom!=null && !(ATTR_KEY in dom);
45 }
46
47 let ret = idiff(dom, vnode, context, mountAll, componentRoot);
48
49 // append the element if its a new parent
50 if (parent && ret.parentNode!==parent) parent.appendChild(ret);
51
52 // diffLevel being reduced to 0 means we're exiting the diff
53 if (!--diffLevel) {
54 hydrating = false;
55 // invoke queued componentDidMount lifecycle methods
56 if (!componentRoot) flushMounts();
57 }
58
59 return ret;
60}
61
62
63/** Internals of `diff()`, separated to allow bypassing diffLevel / mount flushing. */
64function idiff(dom, vnode, context, mountAll, componentRoot) {
65 let out = dom,
66 prevSvgMode = isSvgMode;
67
68 // empty values (null, undefined, booleans) render as empty Text nodes
69 if (vnode==null || typeof vnode==='boolean') vnode = '';
70
71
72 // Fast case: Strings & Numbers create/update Text nodes.
73 if (typeof vnode==='string' || typeof vnode==='number') {
74
75 // update if it's already a Text node:
76 if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
77 /* istanbul ignore if */ /* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
78 if (dom.nodeValue!=vnode) {
79 dom.nodeValue = vnode;
80 }
81 }
82 else {
83 // it wasn't a Text node: replace it with one and recycle the old Element
84 out = document.createTextNode(vnode);
85 if (dom) {
86 if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
87 recollectNodeTree(dom, true);
88 }
89 }
90
91 out[ATTR_KEY] = true;
92
93 return out;
94 }
95
96
97 // If the VNode represents a Component, perform a component diff:
98 let vnodeName = vnode.nodeName;
99 if (typeof vnodeName==='function') {
100 return buildComponentFromVNode(dom, vnode, context, mountAll);
101 }
102
103
104 // Tracks entering and exiting SVG namespace when descending through the tree.
105 isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
106
107
108 // If there's no existing element or it's the wrong type, create a new one:
109 vnodeName = String(vnodeName);
110 if (!dom || !isNamedNode(dom, vnodeName)) {
111 out = createNode(vnodeName, isSvgMode);
112
113 if (dom) {
114 // move children into the replacement node
115 while (dom.firstChild) out.appendChild(dom.firstChild);
116
117 // if the previous Element was mounted into the DOM, replace it inline
118 if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
119
120 // recycle the old element (skips non-Element node types)
121 recollectNodeTree(dom, true);
122 }
123 }
124
125
126 let fc = out.firstChild,
127 props = out[ATTR_KEY],
128 vchildren = vnode.children;
129
130 if (props==null) {
131 props = out[ATTR_KEY] = {};
132 for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
133 }
134
135 // Optimization: fast-path for elements containing a single TextNode:
136 if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
137 if (fc.nodeValue!=vchildren[0]) {
138 fc.nodeValue = vchildren[0];
139 }
140 }
141 // otherwise, if there are existing or new children, diff them:
142 else if (vchildren && vchildren.length || fc!=null) {
143 innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
144 }
145
146
147 // Apply attributes/props from VNode to the DOM Element:
148 diffAttributes(out, vnode.attributes, props);
149
150
151 // restore previous SVG mode: (in case we're exiting an SVG namespace)
152 isSvgMode = prevSvgMode;
153
154 return out;
155}
156
157
158/** Apply child and attribute changes between a VNode and a DOM Node to the DOM.
159 * @param {Element} dom Element whose children should be compared & mutated
160 * @param {Array} vchildren Array of VNodes to compare to `dom.childNodes`
161 * @param {Object} context Implicitly descendant context object (from most recent `getChildContext()`)
162 * @param {Boolean} mountAll
163 * @param {Boolean} isHydrating If `true`, consumes externally created elements similar to hydration
164 */
165function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
166 let originalChildren = dom.childNodes,
167 children = [],
168 keyed = {},
169 keyedLen = 0,
170 min = 0,
171 len = originalChildren.length,
172 childrenLen = 0,
173 vlen = vchildren ? vchildren.length : 0,
174 j, c, f, vchild, child;
175
176 // Build up a map of keyed children and an Array of unkeyed children:
177 if (len!==0) {
178 for (let i=0; i<len; i++) {
179 let child = originalChildren[i],
180 props = child[ATTR_KEY],
181 key = vlen && props ? child._component ? child._component.__key : props.key : null;
182 if (key!=null) {
183 keyedLen++;
184 keyed[key] = child;
185 }
186 else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
187 children[childrenLen++] = child;
188 }
189 }
190 }
191
192 if (vlen!==0) {
193 for (let i=0; i<vlen; i++) {
194 vchild = vchildren[i];
195 child = null;
196
197 // attempt to find a node based on key matching
198 let key = vchild.key;
199 if (key!=null) {
200 if (keyedLen && keyed[key]!==undefined) {
201 child = keyed[key];
202 keyed[key] = undefined;
203 keyedLen--;
204 }
205 }
206 // attempt to pluck a node of the same type from the existing children
207 else if (!child && min<childrenLen) {
208 for (j=min; j<childrenLen; j++) {
209 if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
210 child = c;
211 children[j] = undefined;
212 if (j===childrenLen-1) childrenLen--;
213 if (j===min) min++;
214 break;
215 }
216 }
217 }
218
219 // morph the matched/found/created DOM child to match vchild (deep)
220 child = idiff(child, vchild, context, mountAll);
221
222 f = originalChildren[i];
223 if (child && child!==dom && child!==f) {
224 if (f==null) {
225 dom.appendChild(child);
226 }
227 else if (child===f.nextSibling) {
228 removeNode(f);
229 }
230 else {
231 dom.insertBefore(child, f);
232 }
233 }
234 }
235 }
236
237
238 // remove unused keyed children:
239 if (keyedLen) {
240 for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
241 }
242
243 // remove orphaned unkeyed children:
244 while (min<=childrenLen) {
245 if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
246 }
247}
248
249
250
251/** Recursively recycle (or just unmount) a node and its descendants.
252 * @param {Node} node DOM node to start unmount/removal from
253 * @param {Boolean} [unmountOnly=false] If `true`, only triggers unmount lifecycle, skips removal
254 */
255export function recollectNodeTree(node, unmountOnly) {
256 let component = node._component;
257 if (component) {
258 // if node is owned by a Component, unmount that component (ends up recursing back here)
259 unmountComponent(component);
260 }
261 else {
262 // If the node's VNode had a ref function, invoke it with null here.
263 // (this is part of the React spec, and smart for unsetting references)
264 if (node[ATTR_KEY]!=null && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null);
265
266 if (unmountOnly===false || node[ATTR_KEY]==null) {
267 removeNode(node);
268 }
269
270 removeChildren(node);
271 }
272}
273
274
275/** Recollect/unmount all children.
276 * - we use .lastChild here because it causes less reflow than .firstChild
277 * - it's also cheaper than accessing the .childNodes Live NodeList
278 */
279export function removeChildren(node) {
280 node = node.lastChild;
281 while (node) {
282 let next = node.previousSibling;
283 recollectNodeTree(node, true);
284 node = next;
285 }
286}
287
288
289/** Apply differences in attributes from a VNode to the given DOM Element.
290 * @param {Element} dom Element with attributes to diff `attrs` against
291 * @param {Object} attrs The desired end-state key-value attribute pairs
292 * @param {Object} old Current/previous attributes (from previous VNode or element's prop cache)
293 */
294function diffAttributes(dom, attrs, old) {
295 let name;
296
297 // remove attributes no longer present on the vnode by setting them to undefined
298 for (name in old) {
299 if (!(attrs && attrs[name]!=null) && old[name]!=null) {
300 setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
301 }
302 }
303
304 // add new & update changed attributes
305 for (name in attrs) {
306 if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
307 setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
308 }
309 }
310}