From d08a0356d27a23788d42f5c8a0ee481bc262f633 Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 15 Aug 2019 11:07:03 +0200 Subject: [PATCH 1/2] readme.rst: fixed a typo and made the snippet runnable --- readme.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index bf5058b..4b8cec2 100644 --- a/readme.rst +++ b/readme.rst @@ -338,7 +338,7 @@ modules can be used since there is no JS interpreter. const places = @["boston", "cleveland", "los angeles", "new orleans"] proc render*(): string = - let node = buildHtml(tdiv(class = "mt-3")): + let vnode = buildHtml(tdiv(class = "mt-3")): h1: text "My Web Page" p: text "Hello world" ul: @@ -351,3 +351,5 @@ modules can be used since there is no JS interpreter. dt: text "Can I use Karax for server side HTML rendering?" dd: text "Yes" result = $vnode + + echo render() From e422087217ff3bbedfd23baaa2b2e713f47e3ecf Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 15 Aug 2019 17:15:07 +0200 Subject: [PATCH 2/2] avoid exponential DOM diffing --- karax/karax.nim | 213 ++++++++++++++++++++++++--------------------- tests/difftest.nim | 20 +++-- 2 files changed, 124 insertions(+), 109 deletions(-) diff --git a/karax/karax.nim b/karax/karax.nim index 82a7ee2..6c69325 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -13,11 +13,11 @@ proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.} type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame Patch = object k: PatchKind parent, current: Node - n: VNode + newNode, oldNode: VNode PatchV = object parent, newChild: VNode pos: int @@ -117,7 +117,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; # --------------------- DOM diff ----------------------------------------- template detach(n: VNode) = - addPatch(kxi, pkDetach, nil, nil, n) + addPatch(kxi, pkDetach, nil, nil, nil, n) template attach(n: VNode) = n.dom = result @@ -134,7 +134,9 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode = if kxi.byId.contains(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: result = document.createTextNode(n.text) attach n @@ -145,7 +147,7 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = return result elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) - result = vnodeToDom(x, kxi) + result = toDom(x, useAttachedNode, kxi) #n.key = result.key attach n return result @@ -163,14 +165,14 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = x.expanded = x.renderImpl(x) # x.updatedImpl(x, nil) assert x.expanded != nil - result = vnodeToDom(x.expanded, kxi) + result = toDom(x.expanded, useAttachedNode, kxi) attach n return result else: result = document.createElement(toTag[n.kind]) attach n for k in n: - appendChild(result, vnodeToDom(k, kxi)) + appendChild(result, toDom(k, useAttachedNode, kxi)) # text is mapped to 'value': if n.text != nil: result.value = n.text @@ -231,7 +233,7 @@ when defined(profileKarax): for i in low(DifferEnum)..high(DifferEnum): echo i, " value: ", a[i] -proc eq(a, b: VNode): EqResult = +proc eq(a, b: VNode; recursive: bool): EqResult = if a.kind != b.kind: when defined(profileKarax): inc reasons[deKind] return different @@ -250,7 +252,7 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: return different if a.len != b.len: return different for i in 0..= kxi.patches.len: # 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: kxi.patches[L].k = ka kxi.patches[L].parent = parenta kxi.patches[L].current = currenta - kxi.patches[L].n = na + kxi.patches[L].newNode = na + kxi.patches[L].oldNode = oldNode inc kxi.patchLen 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 inc kxi.patchLenV +proc moveDom(dest, src: VNode) = + dest.dom = src.dom + src.dom = nil + assert dest.len == src.len + for i in 0.. 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): 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: echo oldNode.text - #return - #doAssert false, "overflow!" inc kxi.recursion - result = eq(newNode, oldNode) + let result = eq(newNode, oldNode, false) case result of componentsIdentical: 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.dom.nodeValue = newNode.text - if newNode.events.len != 0 or oldNode.events.len != 0: - 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 + #if newNode.events.len != 0 or oldNode.events.len != 0: + # mergeEvents(newNode, oldNode, kxi) let newLength = newNode.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) assert oldNode.kind == newNode.kind 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: 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 + else: + break # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 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 newPos + else: + break let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: for i in commonPrefix..pos-1: - let r = 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 + diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) if oldPos + 1 == oldLength: for i in pos..newPos: - kxi.addPatch(pkAppend, current, nil, newNode[i]) - result = usenewNode + kxi.addPatch(pkAppend, current, nil, newNode[i], nil) else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - kxi.addPatch(pkInsertBefore, current, before, newNode[i]) - result = usenewNode + kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) #doAssert i < current.childNodes.len - kxi.addPatch(pkRemove, current, current.childNodes[i], nil) - result = usenewNode + kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil) of different: detach(oldNode) - kxi.addPatch(pkReplace, parent, current, newNode) + kxi.addPatch(pkReplace, parent, current, newNode, nil) of usenewNode: doAssert(false, "eq returned usenewNode") when defined(stats): dec kxi.recursion @@ -517,24 +525,25 @@ proc applyComponents(kxi: KaraxInstance) = x.renderedVersion = x.version if oldExpanded == nil: detach(x) - kxi.addPatch(pkReplace, parent, current, x.expanded) + kxi.addPatch(pkReplace, parent, current, x.expanded, nil) when defined(karaxDebug): echo "Component ", newNode.text, ": old expansion didn't exist" else: - let res = diff(x.expanded, oldExpanded, parent, current, kxi) - if res == usenewNode: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" - discard "diff created a patchset for us, so this is fine" - elif res != different: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" - x.expanded = oldExpanded - assert oldExpanded.dom != nil, "old expanded.dom is nil" - else: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" - assert x.expanded.dom != nil, "expanded.dom is nil" + diff(x.expanded, oldExpanded, parent, current, kxi) + when false: + if res == usenewNode: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" + discard "diff created a patchset for us, so this is fine" + elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" + assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) @@ -549,7 +558,7 @@ when defined(stats): proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = detach(parent[position]) let current = parent.dom - kxi.addPatch(pkRemove, current, current.childNodes[position], nil) + kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil) parent.delete(position) applyPatch(kxi) 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) = let current = parent.dom if position >= parent.len: - kxi.addPatch(pkAppend, current, nil, kid) + kxi.addPatch(pkAppend, current, nil, kid, nil) parent.add(kid) else: let before = current.childNodes[position] - kxi.addPatch(pkInsertBefore, current, before, kid) + kxi.addPatch(pkInsertBefore, current, before, kid, nil) parent.insert(kid, position) applyPatch(kxi) 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) = let olddom = oldNode.dom 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 # the current VNode (because we don't store it at all!), we # need to override the fields individually: @@ -591,13 +600,12 @@ proc dodraw(kxi: KaraxInstance) = newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - let asdom = vnodeToDom(newtree, kxi) + let asdom = toDom(newtree, useAttachedNode = true, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) - discard diff(newtree, kxi.currentTree, nil, olddom, kxi) - #kout cstring"patch len ", patches.len + diff(newtree, kxi.currentTree, nil, olddom, kxi) when defined(profileKarax): echo "<<<<<<<<<<<<<<" echa reasons @@ -768,3 +776,6 @@ proc toChecked*(checked: bool): cstring = proc toDisabled*(disabled: bool): cstring = (if disabled: cstring"disabled" else: cstring(nil)) + +proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = + result = toDom(n, useAttachedNode = false, kxi) diff --git a/tests/difftest.nim b/tests/difftest.nim index fca3391..aa2a3c0 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -22,15 +22,19 @@ proc shortRepr(n: VNode): string = var err = 0 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..= expected.len: - echo "patches differ; expected nothing but got: ", p - inc err - elif p != expected[i]: - echo "patches differ; expected ", expected[i], " but got: ", p - inc err + if kxi.patches[i].k != pkSame: + 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 + inc err + elif p != expected[j]: + echo "patches differ; expected ", expected[i], " but got: ", p + inc err + inc j #hasDom(kxi.currentTree) kxi.patchLen = 0