From 7cfd801e4c33a058e54ff8411700249abb7aab0d Mon Sep 17 00:00:00 2001 From: omochimetaru Date: Thu, 25 Apr 2024 00:33:26 +0900 Subject: [PATCH 1/2] wip; use map for match --- Sources/React/Renderer/ReactRoot.swift | 44 ++++----- Sources/React/VDOM/VNode.swift | 10 ++ Sources/React/VDOM/VNodeMatch.swift | 131 +++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 23 deletions(-) create mode 100644 Sources/React/VDOM/VNodeMatch.swift diff --git a/Sources/React/Renderer/ReactRoot.swift b/Sources/React/Renderer/ReactRoot.swift index 5d7a1f9..afa1f8d 100644 --- a/Sources/React/Renderer/ReactRoot.swift +++ b/Sources/React/Renderer/ReactRoot.swift @@ -382,34 +382,31 @@ public final class ReactRoot { ) throws { var patchedOldChildren: [VNode?] = oldChildren - let diff = newChildren.difference(from: oldChildren) - .inferringMoves() + let match = VNode.match(newChildren: newChildren, oldChildren: oldChildren) - var nextIndex = 0 - - for patch in diff { - switch patch { - case .remove(offset: let offset, element: let oldNode, associatedWith: let dest): - patchedOldChildren.remove(at: offset) + for remove in match.removes { + patchedOldChildren.remove(at: remove.offset) - if let _ = dest { - // process on insert - } else { - try renderNode(new: nil, old: oldNode) - } - case .insert(offset: let offset, element: let newNode, associatedWith: let source): - for index in nextIndex.. VNodeMatch { + matchStdlib(newChildren: newChildren, oldChildren: oldChildren) + } + + static func matchFast(newChildren: [VNode], oldChildren: [VNode]) { + var oldDest: [Int?] = Array(repeating: nil, count: oldChildren.count) + var newSource: [Int?] = Array(repeating: nil, count: newChildren.count) + + var newKeyTable: [VNodeDiscriminator: Int] = [:] + for (index, new) in newChildren.enumerated() { + if new.discriminator.key != nil, + newKeyTable[new.discriminator] == nil + { + newKeyTable[new.discriminator] = index + } + } + + for (oldIndex, old) in oldChildren.enumerated() { + if old.discriminator.key != nil { + if let newIndex = newKeyTable[old.discriminator] { + oldDest[oldIndex] = newIndex + newSource[newIndex] = oldIndex + } + } + } + + var newTable: [VNodeDiscriminator: [Int]] = [:] + for (index, new) in newChildren.enumerated() { + if newSource[index] != nil { continue } + newTable[new.discriminator, default: []].append(index) + } + + for (oldIndex, old) in oldChildren.enumerated() { + if oldDest[oldIndex] != nil { continue } + + if var array = newTable[old.discriminator], !array.isEmpty { + let newIndex = array.removeFirst() + newTable[old.discriminator] = array + + oldDest[oldIndex] = newIndex + newSource[newIndex] = oldIndex + } + } + + for (oldIndex, old) in oldChildren.enumerated().reversed() { + if oldDest[oldIndex] == nil { + // remove operation + + // offset + newSource = newSource.map { (source) in + if let source, + source >= oldIndex + { + return source - 1 + } else { + return source + } + } + } + } + + // ...? + } + + static func matchStdlib(newChildren: [VNode], oldChildren: [VNode]) -> VNodeMatch { + let diff = newChildren + .difference(from: oldChildren) + .inferringMoves() + + var removes: [VNodeMatchRemove] = [] + var inserts: [VNodeMatchInsert] = [] + + for patch in diff { + switch patch { + case .remove(offset: let offset, element: let oldNode, associatedWith: let dest): + let x = VNodeMatchRemove( + offset: offset, + node: oldNode, + isMove: dest != nil + ) + removes.append(x) + case .insert(offset: let offset, element: let newNode, associatedWith: let source): + let x = VNodeMatchInsert( + offset: offset, + newNode: newNode, + oldNode: source.map { oldChildren[$0] } + ) + inserts.append(x) + } + } + + return VNodeMatch(removes: removes, inserts: inserts) + } +} From b4f1a48013cd65a4a7bcc687b6867d436fd155ca Mon Sep 17 00:00:00 2001 From: omochimetaru Date: Thu, 25 Apr 2024 01:34:14 +0900 Subject: [PATCH 2/2] VNodeDiscriminator --- Sources/React/Renderer/ReactRoot.swift | 52 ++++----- Sources/React/VDOM/VNode.swift | 42 +------- Sources/React/VDOM/VNodeMatch.swift | 141 ++++++++----------------- 3 files changed, 70 insertions(+), 165 deletions(-) diff --git a/Sources/React/Renderer/ReactRoot.swift b/Sources/React/Renderer/ReactRoot.swift index afa1f8d..c372b9f 100644 --- a/Sources/React/Renderer/ReactRoot.swift +++ b/Sources/React/Renderer/ReactRoot.swift @@ -382,40 +382,42 @@ public final class ReactRoot { ) throws { var patchedOldChildren: [VNode?] = oldChildren - let match = VNode.match(newChildren: newChildren, oldChildren: oldChildren) - - for remove in match.removes { - patchedOldChildren.remove(at: remove.offset) - - if remove.isMove { - // process on insert - } else { - try renderNode(new: nil, old: remove.node) - } - } + let diff = VNode.match(newChildren: newChildren, oldChildren: oldChildren) var nextIndex = 0 - for insert in match.inserts { - for index in nextIndex.. Bool { - a.equality == b.equality - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(equality) - } - public var parent: VNode? { _parent } public func removeFromParent() { diff --git a/Sources/React/VDOM/VNodeMatch.swift b/Sources/React/VDOM/VNodeMatch.swift index 54a8e00..8698104 100644 --- a/Sources/React/VDOM/VNodeMatch.swift +++ b/Sources/React/VDOM/VNodeMatch.swift @@ -4,8 +4,22 @@ enum VNodeKind: Hashable { case component(ObjectIdentifier) } +extension Component { + var vnodeKind: VNodeKind { + switch self { + case let html as HTMLElement: .html(html.tagName) + case is TextElement: .text + default: .component(ObjectIdentifier(type(of: self))) + } + } + + var vnodeDiscriminator: VNodeDiscriminator { + VNodeDiscriminator(kind: vnodeKind, key: key) + } +} + struct VNodeDiscriminator: Hashable { - public init( + init( kind: VNodeKind, key: AnyHashable? ) { @@ -13,119 +27,48 @@ struct VNodeDiscriminator: Hashable { self.key = key } - public var kind: VNodeKind - public var key: AnyHashable? + var kind: VNodeKind + var key: AnyHashable? } -struct VNodeMatchRemove { - var offset: Int +struct VNodeMatchAdapter: Hashable { + init(node: VNode) { + self.node = node + self.disc = node.component.vnodeDiscriminator + } + var node: VNode - var isMove: Bool -} + let disc: VNodeDiscriminator -struct VNodeMatchInsert { - var offset: Int - var newNode: VNode - var oldNode: VNode? -} + static func ==(a: Self, b: Self) -> Bool { a.disc == b.disc } -struct VNodeMatch { - var removes: [VNodeMatchRemove] - var inserts: [VNodeMatchInsert] + func hash(into hasher: inout Hasher) { + hasher.combine(disc) + } } extension VNode { - static func match(newChildren: [VNode], oldChildren: [VNode]) -> VNodeMatch { + static func match(newChildren: [VNode], oldChildren: [VNode]) -> CollectionDifference { matchStdlib(newChildren: newChildren, oldChildren: oldChildren) } - static func matchFast(newChildren: [VNode], oldChildren: [VNode]) { - var oldDest: [Int?] = Array(repeating: nil, count: oldChildren.count) - var newSource: [Int?] = Array(repeating: nil, count: newChildren.count) - - var newKeyTable: [VNodeDiscriminator: Int] = [:] - for (index, new) in newChildren.enumerated() { - if new.discriminator.key != nil, - newKeyTable[new.discriminator] == nil - { - newKeyTable[new.discriminator] = index - } - } - - for (oldIndex, old) in oldChildren.enumerated() { - if old.discriminator.key != nil { - if let newIndex = newKeyTable[old.discriminator] { - oldDest[oldIndex] = newIndex - newSource[newIndex] = oldIndex - } - } - } + static func matchStdlib(newChildren: [VNode], oldChildren: [VNode]) -> CollectionDifference { + let newChildren = newChildren.map { VNodeMatchAdapter(node: $0) } + let oldChildren = oldChildren.map { VNodeMatchAdapter(node: $0) } - var newTable: [VNodeDiscriminator: [Int]] = [:] - for (index, new) in newChildren.enumerated() { - if newSource[index] != nil { continue } - newTable[new.discriminator, default: []].append(index) - } - - for (oldIndex, old) in oldChildren.enumerated() { - if oldDest[oldIndex] != nil { continue } - - if var array = newTable[old.discriminator], !array.isEmpty { - let newIndex = array.removeFirst() - newTable[old.discriminator] = array - - oldDest[oldIndex] = newIndex - newSource[newIndex] = oldIndex - } - } - - for (oldIndex, old) in oldChildren.enumerated().reversed() { - if oldDest[oldIndex] == nil { - // remove operation - - // offset - newSource = newSource.map { (source) in - if let source, - source >= oldIndex - { - return source - 1 - } else { - return source - } - } - } - } - - // ...? - } - - static func matchStdlib(newChildren: [VNode], oldChildren: [VNode]) -> VNodeMatch { - let diff = newChildren + let diff: CollectionDifference = newChildren .difference(from: oldChildren) .inferringMoves() - var removes: [VNodeMatchRemove] = [] - var inserts: [VNodeMatchInsert] = [] - - for patch in diff { - switch patch { - case .remove(offset: let offset, element: let oldNode, associatedWith: let dest): - let x = VNodeMatchRemove( - offset: offset, - node: oldNode, - isMove: dest != nil - ) - removes.append(x) - case .insert(offset: let offset, element: let newNode, associatedWith: let source): - let x = VNodeMatchInsert( - offset: offset, - newNode: newNode, - oldNode: source.map { oldChildren[$0] } - ) - inserts.append(x) + return CollectionDifference( + diff.map { (patch) in + switch patch { + case .remove(offset: let o, element: let e, associatedWith: let a): + .remove(offset: o, element: e.node, associatedWith: a) + case .insert(offset: let o, element: let e, associatedWith: let a): + .insert(offset: o, element: e.node, associatedWith: a) + } } - } - - return VNodeMatch(removes: removes, inserts: inserts) + )! } }