[render] cleanup and comments
This commit is contained in:
parent
144ce68192
commit
285cb5382f
2 changed files with 69 additions and 53 deletions
|
|
@ -33,7 +33,8 @@
|
||||||
- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966))
|
- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966))
|
||||||
- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
||||||
- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
||||||
- Internals: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
|
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
|
||||||
|
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
|
||||||
|
|
||||||
#### Bug fixes
|
#### Bug fixes
|
||||||
|
|
||||||
|
|
|
||||||
119
render/render.js
119
render/render.js
|
|
@ -200,39 +200,47 @@ module.exports = function($window) {
|
||||||
|
|
||||||
// ## Diffing
|
// ## Diffing
|
||||||
//
|
//
|
||||||
// If one list is keyed and the other is unkeyed, the old is removed, and the new one is
|
// Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837
|
||||||
// inserted (since the keys are guaranteed to differ).
|
// may be good for context on longest increasing subsequence-based logic for moving nodes.
|
||||||
//
|
//
|
||||||
// Then comes the unkeyed diff algo, and at last, the keyed diff algorithm that is split
|
// In order to diff keyed lists, one has to
|
||||||
// in four parts (simplifying a bit).
|
|
||||||
//
|
//
|
||||||
// The first part goes through both lists top-down as long as the nodes at each level have
|
// 1) match nodes in both lists, per key, and update them accordingly
|
||||||
// the same key.
|
// 2) create the nodes present in the new list, but absent in the old one
|
||||||
|
// 3) remove the nodes present in the old list, but absent in the new one
|
||||||
|
// 4) figure out what nodes in 1) to move in order to minimize the DOM operations.
|
||||||
//
|
//
|
||||||
// The second part deals with lists reversals, and traverses one list top-down and the other
|
// To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate
|
||||||
// bottom-up (as long as the keys match).
|
// over the new list and for each new vnode, find the corresponding vnode in the old list using
|
||||||
|
// the map.
|
||||||
|
// 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new
|
||||||
|
// and must be created.
|
||||||
|
// For the removals, we actually remove the nodes that have been updated from the old list.
|
||||||
|
// The nodes that remain in that list after 1) and 2) have been performed can be safely removed.
|
||||||
|
// The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS)
|
||||||
|
// algorithm.
|
||||||
//
|
//
|
||||||
// The third part goes through both lists bottom up as long as the keys match.
|
// the longest increasing subsequence is the list of nodes that can remain in place. Imagine going
|
||||||
|
// from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices
|
||||||
|
// corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would
|
||||||
|
// match the above lists, for example).
|
||||||
//
|
//
|
||||||
// The first and third sections allow us to deal efficiently with situations where one or
|
// In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We
|
||||||
// more contiguous nodes were either inserted into, removed from or re-ordered in an otherwise
|
// can update those nodes without moving them, and only call `insertNode` on `4` and `5`.
|
||||||
// sorted list. They may reduce the number of nodes to be processed in the fourth section.
|
|
||||||
//
|
//
|
||||||
// The fourth section does keyed diff for the situations not covered by the other three. It
|
// @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually
|
||||||
// builds a {key: oldIndex} dictionary and uses it to find old nodes that match the keys of
|
// the longest increasing subsequence *of old nodes still present in the new list*).
|
||||||
// new ones.
|
|
||||||
// The nodes from the `old` array that have a match in the new `vnodes` one are removed from
|
|
||||||
// the old list (set to `null`).
|
|
||||||
//
|
//
|
||||||
// If there are still nodes in the new `vnodes` array that haven't been matched to old ones,
|
// It is a general algorithm that is fireproof in all circumstances, but it requires the allocation
|
||||||
// they are created.
|
// and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`,
|
||||||
// The range of old nodes that wasn't covered by the first three sections is passed to
|
// the `LIS` and a temporary one to create the LIS).
|
||||||
// `removeNodes()`. The nodes that remain in the list are removed from the DOM.
|
//
|
||||||
|
// So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of
|
||||||
|
// the LIS and can be updated without moving them.
|
||||||
|
//
|
||||||
|
// If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with
|
||||||
|
// the exception of the last node if the list is fully reversed).
|
||||||
//
|
//
|
||||||
// It should be noted that the description of the four sections above is not perfect, because those
|
|
||||||
// parts are actually implemented as only two loops, one for the first two parts, and one for
|
|
||||||
// the other two. I'm not sure it wins us anything except maybe a few bytes of file size.
|
|
||||||
|
|
||||||
// ## Finding the next sibling.
|
// ## Finding the next sibling.
|
||||||
//
|
//
|
||||||
// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations.
|
// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations.
|
||||||
|
|
@ -301,6 +309,7 @@ module.exports = function($window) {
|
||||||
// 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, oe, ve, topSibling
|
||||||
|
|
||||||
|
// bottom-up
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
oe = old[oldEnd]
|
oe = old[oldEnd]
|
||||||
ve = vnodes[end]
|
ve = vnodes[end]
|
||||||
|
|
@ -314,6 +323,7 @@ module.exports = function($window) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// top-down
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
o = old[oldStart]
|
o = old[oldStart]
|
||||||
v = vnodes[start]
|
v = vnodes[start]
|
||||||
|
|
@ -326,11 +336,8 @@ module.exports = function($window) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swaps and list reversals
|
||||||
//swaps and list reversals
|
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
o = old[oldStart]
|
|
||||||
v = vnodes[start]
|
|
||||||
if (o == null) oldStart++
|
if (o == null) oldStart++
|
||||||
else if (v == null) start++
|
else if (v == null) start++
|
||||||
else if (oe == null) oldEnd--
|
else if (oe == null) oldEnd--
|
||||||
|
|
@ -348,7 +355,10 @@ module.exports = function($window) {
|
||||||
}
|
}
|
||||||
oe = old[oldEnd]
|
oe = old[oldEnd]
|
||||||
ve = vnodes[end]
|
ve = vnodes[end]
|
||||||
|
o = old[oldStart]
|
||||||
|
v = vnodes[start]
|
||||||
}
|
}
|
||||||
|
// bottom up once again
|
||||||
while (oldEnd >= oldStart && end >= start) {
|
while (oldEnd >= oldStart && end >= start) {
|
||||||
if (oe == null) oldEnd--
|
if (oe == null) oldEnd--
|
||||||
else if (ve == null) end--
|
else if (ve == null) end--
|
||||||
|
|
@ -365,47 +375,48 @@ module.exports = function($window) {
|
||||||
if (start > end) removeNodes(old, oldStart, oldEnd + 1)
|
if (start > end) removeNodes(old, oldStart, oldEnd + 1)
|
||||||
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||||
else {
|
else {
|
||||||
// inspired by ivi
|
// inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
|
||||||
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, map, i, lis
|
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices
|
||||||
var oldIndices = new Array(end - start + 1)
|
|
||||||
for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
|
for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
|
||||||
var pos = 2147483647, matched = false
|
|
||||||
|
|
||||||
for (i = end; i >= start; i--) {
|
for (i = end; i >= start; i--) {
|
||||||
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
|
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
|
||||||
v = vnodes[i]
|
ve = vnodes[i]
|
||||||
if (v != null) {
|
if (ve != null) {
|
||||||
var oldIndex = map[v.key]
|
var oldIndex = map[ve.key]
|
||||||
if (oldIndex != null) {
|
if (oldIndex != null) {
|
||||||
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
|
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
|
||||||
oldIndices[i-start] = oldIndex
|
oldIndices[i-start] = oldIndex
|
||||||
o = old[oldIndex]
|
oe = old[oldIndex]
|
||||||
old[oldIndex] = null
|
old[oldIndex] = null
|
||||||
if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns)
|
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
|
||||||
if (v.dom != null) nextSibling = v.dom
|
if (ve.dom != null) nextSibling = ve.dom
|
||||||
matched = true
|
matched++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nextSibling = originalNextSibling
|
nextSibling = originalNextSibling
|
||||||
removeNodes(old, oldStart, oldEnd + 1)
|
if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1)
|
||||||
if (!matched) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||||
else {
|
else {
|
||||||
if (pos === -1) {
|
if (pos === -1) {
|
||||||
lis = makeLis(oldIndices)
|
// the indices of the indices of the items that are part of the
|
||||||
var lisi = lis.length - 1
|
// longest increasing subsequence in the oldIndices list
|
||||||
|
lisIndices = makeLisIndices(oldIndices)
|
||||||
|
li = lisIndices.length - 1
|
||||||
for (i = end; i >= start; i--) {
|
for (i = end; i >= start; i--) {
|
||||||
if (oldIndices[i-start] === -1) createNode(parent, vnodes[i], hooks, ns, nextSibling)
|
v = vnodes[i]
|
||||||
|
if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
|
||||||
else {
|
else {
|
||||||
if (lis[lisi] === i - start) lisi--
|
if (lisIndices[li] === i - start) li--
|
||||||
else insertNode(parent, toFragment(vnodes[i]), nextSibling)
|
else insertNode(parent, toFragment(v), nextSibling)
|
||||||
}
|
}
|
||||||
if (vnodes[i].dom != null) nextSibling = vnodes[i].dom
|
if (v.dom != null) nextSibling = vnodes[i].dom
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (i = end; i >= start; i--) {
|
for (i = end; i >= start; i--) {
|
||||||
if (oldIndices[i-start] === -1) createNode(parent, vnodes[i], hooks, ns, nextSibling)
|
v = vnodes[i]
|
||||||
if (vnodes[i].dom != null) nextSibling = vnodes[i].dom
|
if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
|
||||||
|
if (v.dom != null) nextSibling = vnodes[i].dom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +533,11 @@ module.exports = function($window) {
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
// Lifted from ivi https://github.com/ivijs/ivi/
|
// Lifted from ivi https://github.com/ivijs/ivi/
|
||||||
function makeLis(a) {
|
// takes a list of unique numbers (-1 is special and can
|
||||||
|
// occur multiple times) and returns an array with the indices
|
||||||
|
// of the items that are part of the longest increasing
|
||||||
|
// subsequece
|
||||||
|
function makeLisIndices(a) {
|
||||||
var p = a.slice()
|
var p = a.slice()
|
||||||
var result = []
|
var result = []
|
||||||
result.push(0)
|
result.push(0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue