avoid exponential DOM diffing

This commit is contained in:
Araq 2019-08-15 17:15:07 +02:00 • committed by Andreas Rumpf
commit e422087217
2 changed files with 124 additions and 109 deletions

View file

@ -13,11 +13,11 @@ proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
type type
PatchKind = enum PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
Patch = object Patch = object
k: PatchKind k: PatchKind
parent, current: Node parent, current: Node
n: VNode newNode, oldNode: VNode
PatchV = object PatchV = object
parent, newChild: VNode parent, newChild: VNode
pos: int pos: int
@ -117,7 +117,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
# --------------------- DOM diff ----------------------------------------- # --------------------- DOM diff -----------------------------------------
template detach(n: VNode) = template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, n) addPatch(kxi, pkDetach, nil, nil, nil, n)
template attach(n: VNode) = template attach(n: VNode) =
n.dom = result n.dom = result
@ -134,7 +134,9 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
if kxi.byId.contains(id): if kxi.byId.contains(id):
result = kxi.byId[id] result = kxi.byId[id]
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
if useAttachedNode:
if n.dom != nil: return n.dom
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result = document.createTextNode(n.text) result = document.createTextNode(n.text)
attach n attach n
@ -145,7 +147,7 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
return result return result
elif n.kind == VNodeKind.vthunk: elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n) let x = callThunk(vcomponents[n.text], n)
result = vnodeToDom(x, kxi) result = toDom(x, useAttachedNode, kxi)
#n.key = result.key #n.key = result.key
attach n attach n
return result return result
@ -163,14 +165,14 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
x.expanded = x.renderImpl(x) x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil) # x.updatedImpl(x, nil)
assert x.expanded != nil assert x.expanded != nil
result = vnodeToDom(x.expanded, kxi) result = toDom(x.expanded, useAttachedNode, kxi)
attach n attach n
return result return result
else: else:
result = document.createElement(toTag[n.kind]) result = document.createElement(toTag[n.kind])
attach n attach n
for k in n: for k in n:
appendChild(result, vnodeToDom(k, kxi)) appendChild(result, toDom(k, useAttachedNode, kxi))
# text is mapped to 'value': # text is mapped to 'value':
if n.text != nil: if n.text != nil:
result.value = n.text result.value = n.text
@ -231,7 +233,7 @@ when defined(profileKarax):
for i in low(DifferEnum)..high(DifferEnum): for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i] echo i, " value: ", a[i]
proc eq(a, b: VNode): EqResult = proc eq(a, b: VNode; recursive: bool): EqResult =
if a.kind != b.kind: if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind] when defined(profileKarax): inc reasons[deKind]
return different return different
@ -250,7 +252,7 @@ proc eq(a, b: VNode): EqResult =
if a.text != b.text: return different if a.text != b.text: return different
if a.len != b.len: return different if a.len != b.len: return different
for i in 0..<a.len: for i in 0..<a.len:
if eq(a[i], b[i]) == different: return different if eq(a[i], b[i], recursive) == different: return different
elif a.kind == VNodeKind.dthunk: elif a.kind == VNodeKind.dthunk:
return identical return identical
elif a.kind == VNodeKind.verbatim: elif a.kind == VNodeKind.verbatim:
@ -274,6 +276,14 @@ proc eq(a, b: VNode): EqResult =
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b): if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
when defined(profileKarax): inc reasons[deSimilar] when defined(profileKarax): inc reasons[deSimilar]
return similar return similar
if recursive:
if a.len != b.len:
return different
for i in 0..<a.len:
if eq(a[i], b[i], true) != identical:
return different
# Do not test event listeners here! # Do not test event listeners here!
return result return result
@ -298,6 +308,7 @@ proc updateAttributes(newNode, oldNode: VNode) =
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom let d = oldNode.dom
if d != nil:
for i in 0..<oldNode.events.len: for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0] let k = oldNode.events[i][0]
let name = case k let name = case k
@ -321,16 +332,18 @@ when false:
printV(n[i], depth & " ") printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na: VNode) = na, oldNode: VNode) =
let L = kxi.patchLen let L = kxi.patchLen
if L >= kxi.patches.len: if L >= kxi.patches.len:
# allocate more space: # allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na)) kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
newNode: na, oldNode: oldNode))
else: else:
kxi.patches[L].k = ka kxi.patches[L].k = ka
kxi.patches[L].parent = parenta kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta kxi.patches[L].current = currenta
kxi.patches[L].n = na kxi.patches[L].newNode = na
kxi.patches[L].oldNode = oldNode
inc kxi.patchLen inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
@ -344,26 +357,35 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
kxi.patchesV[L].pos = pos kxi.patchesV[L].pos = pos
inc kxi.patchLenV inc kxi.patchLenV
proc moveDom(dest, src: VNode) =
dest.dom = src.dom
src.dom = nil
assert dest.len == src.len
for i in 0..<dest.len:
moveDom(dest[i], src[i])
proc applyPatch(kxi: KaraxInstance) = proc applyPatch(kxi: KaraxInstance) =
for i in 0..<kxi.patchLen: for i in 0..<kxi.patchLen:
let p = kxi.patches[i] let p = kxi.patches[i]
case p.k case p.k
of pkReplace: of pkReplace:
let nn = vnodeToDom(p.n, kxi) let nn = toDom(p.newNode, useAttachedNode = true, kxi)
if p.parent == nil: if p.parent == nil:
replaceById(kxi.rootId, nn) replaceById(kxi.rootId, nn)
else: else:
p.parent.replaceChild(nn, p.current) p.parent.replaceChild(nn, p.current)
of pkSame:
moveDom(p.newNode, p.oldNode)
of pkRemove: of pkRemove:
p.parent.removeChild(p.current) p.parent.removeChild(p.current)
of pkAppend: of pkAppend:
let nn = vnodeToDom(p.n, kxi) let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.appendChild(nn) p.parent.appendChild(nn)
of pkInsertBefore: of pkInsertBefore:
let nn = vnodeToDom(p.n, kxi) let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.insertBefore(nn, p.current) p.parent.insertBefore(nn, p.current)
of pkDetach: of pkDetach:
let n = p.n let n = p.oldNode
if n.id != nil: kxi.byId.del(n.id) if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component: if n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
@ -377,16 +399,38 @@ proc applyPatch(kxi: KaraxInstance) =
assert p.newChild.dom != nil assert p.newChild.dom != nil
kxi.patchLenV = 0 kxi.patchLenV = 0
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = # ASSUME: We patch both the virtual DOM and the real DOM and throw away
# the newly produced DOM. Thus on updates like 'newNode.dom = oldNode.dom'
# are required. The only exception is when the top level node is replaced.
# Then we have to take the new virtual DOM. In fact, we trigger a full DOM
# rebuild then. However, we don't have to consider old event handlers then
# so everything stays simple.
# We also do not produce "Patch sets" anymore, everything is done as simply
# as possible. Ok, let's assume that we seek to update event handler lists:
# The new node has captures to itself or to other new nodes, never to old
# nodes! --> We cannot ever use the old VDOM, we have to use the new virtual
# DOM. For identical nodes we need to take over the .dom field from the old
# node since we don't recompute them. This must be done recursively. In
# vnodeToDom we have to check whether the 'dom' field was already set. If so,
# There is nothing to do.
# "Similar" nodes can have the opposite effect; consider
#
# AAAABAAAA
# AAAACDAAAA
#
# In this example B did change to C and 'D' is new. However, replacing B by
# CD is fine.
#
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
when defined(stats): when defined(stats):
if kxi.recursion > 100: if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
if oldNode.kind == VNodeKind.text: if oldNode.kind == VNodeKind.text:
echo oldNode.text echo oldNode.text
#return
#doAssert false, "overflow!"
inc kxi.recursion inc kxi.recursion
result = eq(newNode, oldNode) let result = eq(newNode, oldNode, false)
case result case result
of componentsIdentical: of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode), kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
@ -402,92 +446,56 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
oldNode.text = newNode.text oldNode.text = newNode.text
oldNode.dom.nodeValue = newNode.text oldNode.dom.nodeValue = newNode.text
if newNode.events.len != 0 or oldNode.events.len != 0: #if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode, kxi) # mergeEvents(newNode, oldNode, kxi)
when false:
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
if oldNode.text != newNode.text:
oldNode.text = newNode.text
oldNode.dom.value = newNode.text
let newLength = newNode.len let newLength = newNode.len
let oldLength = oldNode.len let oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return result if newLength == 0 and oldLength == 0: return
let minLength = min(newLength, oldLength) let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind assert oldNode.kind == newNode.kind
var commonPrefix = 0 var commonPrefix = 0
let isSpecial = oldNode.kind == VNodeKind.component or
oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
let oldLen = kxi.patchLen
let oldLenV = kxi.patchLenV
assert i < a.len
assert j < b.len
let r = if isSpecial:
diff(a[i], b[j], parent, current, kxi)
else:
diff(a[i], b[j], current, current.childNodes[j], kxi)
case r
of identical, componentsIdentical, similar:
a[i] = b[j]
action
of usenewNode:
kxi.addPatchV(b, j, a[i])
action
# unfortunately, we need to propagate the changes upwards:
result = useNewNode
of different:
# undo what 'diff' would have done:
kxi.patchLen = oldLen
kxi.patchLenV = oldLenV
if result != different: result = r
break
# compute common prefix: # compute common prefix:
while commonPrefix < minLength: while commonPrefix < minLength:
eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
inc commonPrefix inc commonPrefix
else:
break
# compute common suffix: # compute common suffix:
var oldPos = oldLength - 1 var oldPos = oldLength - 1
var newPos = newLength - 1 var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix: while oldPos >= commonPrefix and newPos >= commonPrefix:
eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): if eq(newNode[newPos], oldNode[oldPos], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
dec oldPos dec oldPos
dec newPos dec newPos
else:
break
let pos = min(oldPos, newPos) + 1 let pos = min(oldPos, newPos) + 1
# now the different children are in commonPrefix .. pos - 1: # now the different children are in commonPrefix .. pos - 1:
for i in commonPrefix..pos-1: for i in commonPrefix..pos-1:
let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if r == usenewNode:
#oldNode[i] = newNode[i]
kxi.addPatchV(oldNode, i, newNode[i])
elif r != different:
newNode[i] = oldNode[i]
#else:
# result = usenewNode
if oldPos + 1 == oldLength: if oldPos + 1 == oldLength:
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i]) kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
result = usenewNode
else: else:
let before = current.childNodes[oldPos + 1] let before = current.childNodes[oldPos + 1]
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i]) kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
result = usenewNode
# XXX call 'attach' here? # XXX call 'attach' here?
for i in pos..oldPos: for i in pos..oldPos:
detach(oldNode[i]) detach(oldNode[i])
#doAssert i < current.childNodes.len #doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil) kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
result = usenewNode
of different: of different:
detach(oldNode) detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode) kxi.addPatch(pkReplace, parent, current, newNode, nil)
of usenewNode: doAssert(false, "eq returned usenewNode") of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats): when defined(stats):
dec kxi.recursion dec kxi.recursion
@ -517,11 +525,12 @@ proc applyComponents(kxi: KaraxInstance) =
x.renderedVersion = x.version x.renderedVersion = x.version
if oldExpanded == nil: if oldExpanded == nil:
detach(x) detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded) kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist" echo "Component ", newNode.text, ": old expansion didn't exist"
else: else:
let res = diff(x.expanded, oldExpanded, parent, current, kxi) diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
if res == usenewNode: if res == usenewNode:
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
@ -549,7 +558,7 @@ when defined(stats):
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position]) detach(parent[position])
let current = parent.dom let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil) kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
parent.delete(position) parent.delete(position)
applyPatch(kxi) applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -557,11 +566,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom let current = parent.dom
if position >= parent.len: if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid) kxi.addPatch(pkAppend, current, nil, kid, nil)
parent.add(kid) parent.add(kid)
else: else:
let before = current.childNodes[position] let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid) kxi.addPatch(pkInsertBefore, current, before, kid, nil)
parent.insert(kid, position) parent.insert(kid, position)
applyPatch(kxi) applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -569,7 +578,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom let olddom = oldNode.dom
doAssert olddom != nil doAssert olddom != nil
discard diff(newNode, oldNode, nil, olddom, kxi) diff(newNode, oldNode, nil, olddom, kxi)
# this is a bit nasty: Since we cannot patch the 'parent' of # this is a bit nasty: Since we cannot patch the 'parent' of
# the current VNode (because we don't store it at all!), we # the current VNode (because we don't store it at all!), we
# need to override the fields individually: # need to override the fields individually:
@ -591,13 +600,12 @@ proc dodraw(kxi: KaraxInstance) =
newtree.id = kxi.rootId newtree.id = kxi.rootId
kxi.toFocus = nil kxi.toFocus = nil
if kxi.currentTree == nil: if kxi.currentTree == nil:
let asdom = vnodeToDom(newtree, kxi) let asdom = toDom(newtree, useAttachedNode = true, kxi)
replaceById(kxi.rootId, asdom) replaceById(kxi.rootId, asdom)
else: else:
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
let olddom = document.getElementById(kxi.rootId) let olddom = document.getElementById(kxi.rootId)
discard diff(newtree, kxi.currentTree, nil, olddom, kxi) diff(newtree, kxi.currentTree, nil, olddom, kxi)
#kout cstring"patch len ", patches.len
when defined(profileKarax): when defined(profileKarax):
echo "<<<<<<<<<<<<<<" echo "<<<<<<<<<<<<<<"
echa reasons echa reasons
@ -768,3 +776,6 @@ proc toChecked*(checked: bool): cstring =
proc toDisabled*(disabled: bool): cstring = proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil)) (if disabled: cstring"disabled" else: cstring(nil))
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi)

View file

@ -22,15 +22,19 @@ proc shortRepr(n: VNode): string =
var err = 0 var err = 0
proc doDiff(a, b: VNode; expected: varargs[string]) = proc doDiff(a, b: VNode; expected: varargs[string]) =
discard diff(b, a, nil, vnodeToDom(a, kxi), kxi) diff(b, a, nil, vnodeToDom(a, kxi), kxi)
var j = 0
for i in 0..<kxi.patchLen: for i in 0..<kxi.patchLen:
let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n) if kxi.patches[i].k != pkSame:
if i >= expected.len: let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode
let p = $kxi.patches[i].k & " " & shortRepr(n)
if j >= expected.len:
echo "patches differ; expected nothing but got: ", p echo "patches differ; expected nothing but got: ", p
inc err inc err
elif p != expected[i]: elif p != expected[j]:
echo "patches differ; expected ", expected[i], " but got: ", p echo "patches differ; expected ", expected[i], " but got: ", p
inc err inc err
inc j
#hasDom(kxi.currentTree) #hasDom(kxi.currentTree)
kxi.patchLen = 0 kxi.patchLen = 0