[render/render] Introduce longest increasing subsequence-based node moves in keyed diff, fix #1791, fix #2026
This commit is contained in:
parent
95ec3d28aa
commit
2cfc68359d
1 changed files with 157 additions and 65 deletions
222
render/render.js
222
render/render.js
|
|
@ -260,11 +260,10 @@ module.exports = function($window) {
|
||||||
|
|
||||||
function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
|
function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
|
||||||
if (old === vnodes || old == null && vnodes == null) return
|
if (old === vnodes || old == null && vnodes == null) return
|
||||||
else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
|
else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
|
||||||
else if (vnodes == null) removeNodes(old, 0, old.length)
|
else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length)
|
||||||
else {
|
else {
|
||||||
// default to keyed because, when either list is full of null nodes, it has fewer branches
|
var start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null
|
||||||
var start = 0, oldStart = 0, isOldKeyed = true, isKeyed = true
|
|
||||||
for (; oldStart < old.length; oldStart++) {
|
for (; oldStart < old.length; oldStart++) {
|
||||||
if (old[oldStart] != null) {
|
if (old[oldStart] != null) {
|
||||||
isOldKeyed = old[oldStart].key != null
|
isOldKeyed = old[oldStart].key != null
|
||||||
|
|
@ -277,12 +276,11 @@ module.exports = function($window) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls
|
||||||
if (isOldKeyed !== isKeyed) {
|
if (isOldKeyed !== isKeyed) {
|
||||||
removeNodes(old, oldStart, old.length)
|
removeNodes(old, oldStart, old.length)
|
||||||
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
|
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
|
||||||
return
|
} else if (!isKeyed) {
|
||||||
}
|
|
||||||
if (!isKeyed) {
|
|
||||||
// Don't index past the end of either list (causes deopts).
|
// Don't index past the end of either list (causes deopts).
|
||||||
var commonLength = old.length < vnodes.length ? old.length : vnodes.length
|
var commonLength = old.length < vnodes.length ? old.length : vnodes.length
|
||||||
// Rewind if necessary to the first non-null index on either side.
|
// Rewind if necessary to the first non-null index on either side.
|
||||||
|
|
@ -299,72 +297,120 @@ module.exports = function($window) {
|
||||||
}
|
}
|
||||||
if (old.length > commonLength) removeNodes(old, start, old.length)
|
if (old.length > commonLength) removeNodes(old, start, old.length)
|
||||||
if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
|
if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
|
||||||
return
|
} else {
|
||||||
}
|
// keyed diff
|
||||||
// keyed diff
|
var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling
|
||||||
var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v
|
|
||||||
|
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
// both top-down
|
oe = old[oldEnd]
|
||||||
o = old[oldStart]
|
ve = vnodes[end]
|
||||||
v = vnodes[start]
|
if (oe == null) oldEnd--
|
||||||
if (o == null) oldStart++
|
else if (ve == null) end--
|
||||||
else if (v == null) start++
|
else if (oe.key === ve.key) {
|
||||||
else if (o.key === v.key) {
|
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
|
||||||
oldStart++, start++
|
if (ve.dom != null) nextSibling = ve.dom
|
||||||
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
|
oldEnd--, end--
|
||||||
} else {
|
} else {
|
||||||
// reversal: old top-down, new bottom-up
|
break
|
||||||
v = vnodes[end]
|
|
||||||
if (o == null) oldStart++
|
|
||||||
else if (v == null) end--
|
|
||||||
else if (o.key === v.key) {
|
|
||||||
oldStart++
|
|
||||||
if (start < end--) insertNode(parent, toFragment(o), nextSibling)
|
|
||||||
if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns)
|
|
||||||
if (v.dom != null) nextSibling = v.dom
|
|
||||||
}
|
}
|
||||||
else break
|
|
||||||
}
|
}
|
||||||
}
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
o = old[oldStart]
|
||||||
// both bottom-up
|
v = vnodes[start]
|
||||||
o = old[oldEnd]
|
if (o == null) oldStart++
|
||||||
v = vnodes[end]
|
else if (v == null) start++
|
||||||
if (o == null) oldEnd--
|
else if (o.key === v.key) {
|
||||||
else if (v == null) end--
|
oldStart++, start++
|
||||||
else if (o.key === v.key) {
|
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
|
||||||
if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns)
|
} else {
|
||||||
if (v.dom != null) nextSibling = v.dom
|
break
|
||||||
oldEnd--, end--
|
|
||||||
} else {
|
|
||||||
// old map-based, new bottom-up
|
|
||||||
if (map == null) {
|
|
||||||
// the last node can be left out of the map because it will be caught by the
|
|
||||||
// bottom-up part of the diff loop. If we were to refactor this to use distinct
|
|
||||||
// loops, we'd have to pass `oldEnd + 1` (or change `start < end` to `<=` in getKeyMap).
|
|
||||||
map = getKeyMap(old, oldStart, oldEnd)
|
|
||||||
}
|
}
|
||||||
if (v != null) {
|
}
|
||||||
var oldIndex = map[v.key]
|
|
||||||
if (oldIndex == null) {
|
//swaps and list reversals
|
||||||
createNode(parent, v, hooks, ns, nextSibling)
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
if (v.dom != null) nextSibling = v.dom
|
o = old[oldStart]
|
||||||
} else {
|
v = vnodes[start]
|
||||||
o = old[oldIndex]
|
if (o == null) oldStart++
|
||||||
insertNode(parent, toFragment(o), nextSibling)
|
else if (v == null) start++
|
||||||
if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns)
|
else if (oe == null) oldEnd--
|
||||||
o.skip = true
|
else if (ve == null) end--
|
||||||
if (v.dom != null) nextSibling = v.dom
|
else if (start === end) break
|
||||||
|
else {
|
||||||
|
if (o.key !== ve.key || oe.key !== v.key) break
|
||||||
|
topSibling = getNextSibling(old, oldStart, nextSibling)
|
||||||
|
insertNode(parent, toFragment(oe), topSibling)
|
||||||
|
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
|
||||||
|
if (++start <= --end) insertNode(parent, toFragment(o), nextSibling)
|
||||||
|
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
|
||||||
|
if (ve.dom != null) nextSibling = ve.dom
|
||||||
|
oldStart++; oldEnd--
|
||||||
|
}
|
||||||
|
oe = old[oldEnd]
|
||||||
|
ve = vnodes[end]
|
||||||
|
}
|
||||||
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
|
if (oe == null) oldEnd--
|
||||||
|
else if (ve == null) end--
|
||||||
|
else if (oe.key === ve.key) {
|
||||||
|
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
|
||||||
|
if (ve.dom != null) nextSibling = ve.dom
|
||||||
|
oldEnd--, end--
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
oe = old[oldEnd]
|
||||||
|
ve = vnodes[end]
|
||||||
|
}
|
||||||
|
if (start > end) removeNodes(old, oldStart, oldEnd + 1)
|
||||||
|
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||||
|
else {
|
||||||
|
// inspired by ivi
|
||||||
|
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, map, i, lis
|
||||||
|
var oldIndices = new Array(end - start + 1)
|
||||||
|
for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
|
||||||
|
var pos = 2147483647, matched = false
|
||||||
|
|
||||||
|
for (i = end; i >= start; i--) {
|
||||||
|
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
|
||||||
|
v = vnodes[i]
|
||||||
|
if (v != null) {
|
||||||
|
var oldIndex = map[v.key]
|
||||||
|
if (oldIndex != null) {
|
||||||
|
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
|
||||||
|
oldIndices[i-start] = oldIndex
|
||||||
|
o = old[oldIndex]
|
||||||
|
o.skip = true
|
||||||
|
if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns)
|
||||||
|
if (v.dom != null) nextSibling = v.dom
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextSibling = originalNextSibling
|
||||||
|
removeNodes(old, oldStart, oldEnd + 1)
|
||||||
|
if (!matched) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||||
|
else {
|
||||||
|
if (pos === -1) {
|
||||||
|
lis = makeLis(oldIndices)
|
||||||
|
var lisi = lis.length - 1
|
||||||
|
for (i = end; i >= start; i--) {
|
||||||
|
if (oldIndices[i-start] === -1) createNode(parent, vnodes[i], hooks, ns, nextSibling)
|
||||||
|
else {
|
||||||
|
if (lis[lisi] === i - start) lisi--
|
||||||
|
else insertNode(parent, toFragment(vnodes[i]), nextSibling)
|
||||||
|
}
|
||||||
|
if (vnodes[i].dom != null) nextSibling = vnodes[i].dom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i = end; i >= start; i--) {
|
||||||
|
if (oldIndices[i-start] === -1) createNode(parent, vnodes[i], hooks, ns, nextSibling)
|
||||||
|
if (vnodes[i].dom != null) nextSibling = vnodes[i].dom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end--
|
|
||||||
}
|
}
|
||||||
if (end < start) break
|
|
||||||
}
|
}
|
||||||
// deal with the leftovers.
|
|
||||||
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
|
||||||
removeNodes(old, oldStart, oldEnd + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateNode(parent, old, vnode, hooks, nextSibling, ns) {
|
function updateNode(parent, old, vnode, hooks, nextSibling, ns) {
|
||||||
|
|
@ -475,6 +521,52 @@ module.exports = function($window) {
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
// Lifted from ivi https://github.com/ivijs/ivi/
|
||||||
|
function makeLis(a) {
|
||||||
|
var p = a.slice()
|
||||||
|
var result = []
|
||||||
|
result.push(0)
|
||||||
|
var u
|
||||||
|
var v
|
||||||
|
for (var i = 0, il = a.length; i < il; ++i) {
|
||||||
|
if (a[i] === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var j = result[result.length - 1]
|
||||||
|
if (a[j] < a[i]) {
|
||||||
|
p[i] = j
|
||||||
|
result.push(i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u = 0
|
||||||
|
v = result.length - 1
|
||||||
|
while (u < v) {
|
||||||
|
/*eslint-disable no-bitwise*/
|
||||||
|
var c = ((u + v) / 2) | 0
|
||||||
|
/*eslint-enable no-bitwise*/
|
||||||
|
if (a[result[c]] < a[i]) {
|
||||||
|
u = c + 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
v = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a[i] < a[result[u]]) {
|
||||||
|
if (u > 0) {
|
||||||
|
p[i] = result[u - 1]
|
||||||
|
}
|
||||||
|
result[u] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u = result.length
|
||||||
|
v = result[u - 1]
|
||||||
|
while (u-- > 0) {
|
||||||
|
result[u] = v
|
||||||
|
v = p[v]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
function toFragment(vnode) {
|
function toFragment(vnode) {
|
||||||
var count = vnode.domSize
|
var count = vnode.domSize
|
||||||
if (count != null || vnode.dom == null) {
|
if (count != null || vnode.dom == null) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue