Skip to content

Commit 6c562d2

Browse files
authored
Fix keys, normalize holes (#2452)
* Fix #2434 * Treat holes as unkeyed, normalize boolean/null/undefined This brings a lot better consistency with that API, even though it's slightly breaking. (I had to update a bunch of tests to correspond with it.) * Fill in PR number [skip ci]
1 parent 86d64e2 commit 6c562d2

File tree

12 files changed

+193
-175
lines changed

12 files changed

+193
-175
lines changed

docs/change-log.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@
4545
- Previously, it was only set for all non-`GET` methods and only when `useBody: true` was passed (the default), and it was always set for them. Now it's automatically omitted when no body is present, so the hole is slightly broadened.
4646
- route: query parameters in hash strings are no longer supported ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows))
4747
- It's technically invalid in hashes, so I'd rather push people to keep in line with spec.
48+
- render: validate all elements are either keyed or unkeyed, and treat `null`/`undefined`/booleans as strictly unkeyed ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
49+
- Gives a nice little perf boost with keyed fragments.
50+
- Minor, but imperceptible impact (within the margin of error) with unkeyed fragments.
51+
- Also makes the model a lot more consistent - all values are either keyed or unkeyed.
52+
- vnodes: normalize boolean children to `null`/`undefined` at the vnode level, always stringify non-object children that aren't holes ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
53+
- Previously, `true` was equivalent to `"true"` and `false` was equivalent to `""`.
54+
- Previously, numeric children weren't coerced. Now, they are.
55+
- Unlikely to break most components, but it *could* break some users.
56+
- This increases consistency with how booleans are handled with children, so it should be more intuitive.
4857

4958
#### News
5059

@@ -101,6 +110,7 @@
101110
- route: arbitrary prefixes are properly supported now, including odd prefixes like `?#` and invalid prefixes like `#foo#bar` ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows))
102111
- request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@isiahmeadows](https://github.com/isiahmeadows))
103112
- render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@isiahmeadows](https://github.com/isiahmeadows))
113+
- docs: clarify valid key usage ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
104114

105115
---
106116

docs/hyperscript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ m("button", {
159159

160160
If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent.
161161

162-
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null`or `undefined`, it is ignored.
162+
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null` or `undefined`, it is ignored.
163163

164164
If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`.
165165

docs/keys.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ function correctUserList(users) {
7575
}
7676
```
7777

78+
Also, you might want to reinitialize a component. You can use the common pattern of a single-item keyed fragment where you change the key to destroy and reinitialize the element.
79+
80+
```javascript
81+
function ResettableToggle() {
82+
var toggleKey = false
83+
84+
function reset() {
85+
toggleKey = !toggleKey
86+
}
87+
88+
return {
89+
view: function() {
90+
return [
91+
m("button", {onclick: reset}, "Reset toggle"),
92+
[m(Toggle, {key: toggleKey})]
93+
]
94+
}
95+
}
96+
}
97+
```
98+
7899
---
79100

80101
### Debugging key related issues
@@ -178,6 +199,10 @@ m("div", [
178199
])
179200
```
180201

202+
In fact, this will cause an error to be thrown, to remind you to not do it. So don't do it!
203+
204+
Note that `null`s, `undefined`s, and booleans are considered unkeyed nodes. If you want the keyed equivalent, use `m.fragment({key: ...}, [])`, a keyed empty fragment.
205+
181206
#### Avoid passing model data directly to components if the model uses `key` as a data property
182207

183208
The `key` property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose `key` property is expected to change over time. This can lead to components receiving the wrong data, re-initialize, or change positions unexpectedly. If your data model uses the `key` property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction:

render/hyperscript.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function hyperscript(selector) {
9393
vnode.children = Vnode.normalizeChildren(vnode.children)
9494
if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode)
9595
}
96-
96+
9797
vnode.tag = selector
9898
return vnode
9999
}

render/render.js

Lines changed: 73 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,24 @@ module.exports = function($window) {
4444
return null
4545
}
4646
}
47+
function validateKeys(vnodes, isKeyed) {
48+
// Note: this is a *very* perf-sensitive check.
49+
// Fun fact: merging the loop like this is somehow faster than splitting
50+
// it, noticeably so.
51+
for (var i = 1; i < vnodes.length; i++) {
52+
if ((vnodes[i] != null && vnodes[i].key != null) !== isKeyed) {
53+
throw new TypeError("Vnodes must either always have keys or never have keys!")
54+
}
55+
}
56+
}
4757
//create
48-
function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
58+
function createNodesChecked(parent, vnodes, hooks, nextSibling, ns) {
59+
if (vnodes.length) {
60+
validateKeys(vnodes, vnodes[0] != null && vnodes[0].key != null)
61+
createNodesUnchecked(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
62+
}
63+
}
64+
function createNodesUnchecked(parent, vnodes, start, end, hooks, nextSibling, ns) {
4965
for (var i = start; i < end; i++) {
5066
var vnode = vnodes[i]
5167
if (vnode != null) {
@@ -99,7 +115,7 @@ module.exports = function($window) {
99115
var fragment = $doc.createDocumentFragment()
100116
if (vnode.children != null) {
101117
var children = vnode.children
102-
createNodes(fragment, children, 0, children.length, hooks, null, ns)
118+
createNodesChecked(fragment, children, hooks, null, ns)
103119
}
104120
vnode.dom = fragment.firstChild
105121
vnode.domSize = fragment.childNodes.length
@@ -130,7 +146,7 @@ module.exports = function($window) {
130146
}
131147
if (vnode.children != null) {
132148
var children = vnode.children
133-
createNodes(element, children, 0, children.length, hooks, null, ns)
149+
createNodesChecked(element, children, hooks, null, ns)
134150
if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs)
135151
}
136152
}
@@ -273,26 +289,19 @@ module.exports = function($window) {
273289

274290
function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
275291
if (old === vnodes || old == null && vnodes == null) return
276-
else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
292+
else if (old == null || old.length === 0) createNodesChecked(parent, vnodes, hooks, nextSibling, ns)
277293
else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length)
278294
else {
279-
var start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null
280-
for (; oldStart < old.length; oldStart++) {
281-
if (old[oldStart] != null) {
282-
isOldKeyed = old[oldStart].key != null
283-
break
284-
}
285-
}
286-
for (; start < vnodes.length; start++) {
287-
if (vnodes[start] != null) {
288-
isKeyed = vnodes[start].key != null
289-
break
290-
}
291-
}
295+
var isOldKeyed = old[0] != null && old[0].key != null
296+
var isKeyed = vnodes[0] != null && vnodes[0].key != null
297+
var start = 0, oldStart = 0
298+
validateKeys(vnodes, isKeyed)
299+
if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++
300+
if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++
292301
if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls
293302
if (isOldKeyed !== isKeyed) {
294303
removeNodes(old, oldStart, old.length)
295-
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
304+
createNodesUnchecked(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
296305
} else if (!isKeyed) {
297306
// Don't index past the end of either list (causes deopts).
298307
var commonLength = old.length < vnodes.length ? old.length : vnodes.length
@@ -309,7 +318,7 @@ module.exports = function($window) {
309318
else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns)
310319
}
311320
if (old.length > commonLength) removeNodes(old, start, old.length)
312-
if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
321+
if (vnodes.length > commonLength) createNodesUnchecked(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
313322
} else {
314323
// keyed diff
315324
var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling
@@ -318,90 +327,67 @@ module.exports = function($window) {
318327
while (oldEnd >= oldStart && end >= start) {
319328
oe = old[oldEnd]
320329
ve = vnodes[end]
321-
if (oe == null) oldEnd--
322-
else if (ve == null) end--
323-
else if (oe.key === ve.key) {
324-
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
325-
if (ve.dom != null) nextSibling = ve.dom
326-
oldEnd--, end--
327-
} else {
328-
break
329-
}
330+
if (oe.key !== ve.key) break
331+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
332+
if (ve.dom != null) nextSibling = ve.dom
333+
oldEnd--, end--
330334
}
331335
// top-down
332336
while (oldEnd >= oldStart && end >= start) {
333337
o = old[oldStart]
334338
v = vnodes[start]
335-
if (o == null) oldStart++
336-
else if (v == null) start++
337-
else if (o.key === v.key) {
338-
oldStart++, start++
339-
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
340-
} else {
341-
break
342-
}
339+
if (o.key !== v.key) break
340+
oldStart++, start++
341+
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
343342
}
344343
// swaps and list reversals
345344
while (oldEnd >= oldStart && end >= start) {
346-
if (o == null) oldStart++
347-
else if (v == null) start++
348-
else if (oe == null) oldEnd--
349-
else if (ve == null) end--
350-
else if (start === end) break
351-
else {
352-
if (o.key !== ve.key || oe.key !== v.key) break
353-
topSibling = getNextSibling(old, oldStart, nextSibling)
354-
insertNode(parent, toFragment(oe), topSibling)
355-
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
356-
if (++start <= --end) insertNode(parent, toFragment(o), nextSibling)
357-
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
358-
if (ve.dom != null) nextSibling = ve.dom
359-
oldStart++; oldEnd--
360-
}
345+
if (start === end) break
346+
if (o.key !== ve.key || oe.key !== v.key) break
347+
topSibling = getNextSibling(old, oldStart, nextSibling)
348+
insertNode(parent, toFragment(oe), topSibling)
349+
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
350+
if (++start <= --end) insertNode(parent, toFragment(o), nextSibling)
351+
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
352+
if (ve.dom != null) nextSibling = ve.dom
353+
oldStart++; oldEnd--
361354
oe = old[oldEnd]
362355
ve = vnodes[end]
363356
o = old[oldStart]
364357
v = vnodes[start]
365358
}
366359
// bottom up once again
367360
while (oldEnd >= oldStart && end >= start) {
368-
if (oe == null) oldEnd--
369-
else if (ve == null) end--
370-
else if (oe.key === ve.key) {
371-
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
372-
if (ve.dom != null) nextSibling = ve.dom
373-
oldEnd--, end--
374-
} else {
375-
break
376-
}
361+
if (oe.key !== ve.key) break
362+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
363+
if (ve.dom != null) nextSibling = ve.dom
364+
oldEnd--, end--
377365
oe = old[oldEnd]
378366
ve = vnodes[end]
379367
}
380368
if (start > end) removeNodes(old, oldStart, oldEnd + 1)
381-
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
369+
else if (oldStart > oldEnd) createNodesUnchecked(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
382370
else {
383371
// inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
384372
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices
385373
for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
386374
for (i = end; i >= start; i--) {
387375
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
388376
ve = vnodes[i]
389-
if (ve != null) {
390-
var oldIndex = map[ve.key]
391-
if (oldIndex != null) {
392-
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
393-
oldIndices[i-start] = oldIndex
394-
oe = old[oldIndex]
395-
old[oldIndex] = null
396-
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
397-
if (ve.dom != null) nextSibling = ve.dom
398-
matched++
399-
}
377+
var oldIndex = map[ve.key]
378+
if (oldIndex != null) {
379+
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
380+
oldIndices[i-start] = oldIndex
381+
oe = old[oldIndex]
382+
old[oldIndex] = null
383+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
384+
if (ve.dom != null) nextSibling = ve.dom
385+
matched++
400386
}
401387
}
402388
nextSibling = originalNextSibling
403389
if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1)
404-
if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
390+
if (matched === 0) createNodesUnchecked(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
405391
else {
406392
if (pos === -1) {
407393
// the indices of the indices of the items that are part of the
@@ -541,26 +527,26 @@ module.exports = function($window) {
541527
// occur multiple times) and returns an array with the indices
542528
// of the items that are part of the longest increasing
543529
// subsequece
530+
var lisTemp = []
544531
function makeLisIndices(a) {
545-
var p = a.slice()
546-
var result = []
547-
result.push(0)
548-
var u
549-
var v
550-
for (var i = 0, il = a.length; i < il; ++i) {
551-
if (a[i] === -1) {
552-
continue
553-
}
532+
var result = [0]
533+
var u = 0, v = 0, i = 0
534+
var il = lisTemp.length = a.length
535+
for (var i = 0; i < il; i++) lisTemp[i] = a[i]
536+
for (var i = 0; i < il; ++i) {
537+
if (a[i] === -1) continue
554538
var j = result[result.length - 1]
555539
if (a[j] < a[i]) {
556-
p[i] = j
540+
lisTemp[i] = j
557541
result.push(i)
558542
continue
559543
}
560544
u = 0
561545
v = result.length - 1
562546
while (u < v) {
563-
var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise
547+
// Fast integer average without overflow.
548+
// eslint-disable-next-line no-bitwise
549+
var c = (u >>> 1) + (v >>> 1) + (u & v & 1)
564550
if (a[result[c]] < a[i]) {
565551
u = c + 1
566552
}
@@ -569,18 +555,17 @@ module.exports = function($window) {
569555
}
570556
}
571557
if (a[i] < a[result[u]]) {
572-
if (u > 0) {
573-
p[i] = result[u - 1]
574-
}
558+
if (u > 0) lisTemp[i] = result[u - 1]
575559
result[u] = i
576560
}
577561
}
578562
u = result.length
579563
v = result[u - 1]
580564
while (u-- > 0) {
581565
result[u] = v
582-
v = p[v]
566+
v = lisTemp[v]
583567
}
568+
lisTemp.length = 0
584569
return result
585570
}
586571

0 commit comments

Comments
 (0)