diff --git a/Sources/React/Element/Component.swift b/Sources/React/Element/Component.swift index 12134ab..91b53ec 100644 --- a/Sources/React/Element/Component.swift +++ b/Sources/React/Element/Component.swift @@ -45,3 +45,22 @@ extension Component { extractGhostDefault(input) } } + +enum Components { + static func extractHooks(_ value: Any) -> [any _AnyHookWrapper] { + var hooks: [any _AnyHookWrapper] = [] + + let mirror = Mirror(reflecting: value) + for mc in mirror.children { + switch mc.value { + case let hook as any _AnyHookWrapper: + hooks.append(hook) + case let hook as any Hook: + hooks += extractHooks(hook) + default: break + } + } + + return hooks + } +} diff --git a/Sources/React/Hooks/Context/ContextValueProvider.swift b/Sources/React/Hooks/Context/ContextValueProvider.swift index c27f674..1d37e50 100644 --- a/Sources/React/Hooks/Context/ContextValueProvider.swift +++ b/Sources/React/Hooks/Context/ContextValueProvider.swift @@ -2,7 +2,7 @@ extension ContextValue { public typealias Provider = ContextValueProvider } -public struct ContextValueProvider: Component { +public struct ContextValueProvider: Component & _AnyContextValueProvider { public init( key: AnyHashable? = nil, value: Value, @@ -29,9 +29,17 @@ public struct ContextValueProvider: Component { children } + var _contextValueType: any ContextValue.Type { Value.self } + var _contextValue: any ContextValue { value } + public static func _extractGhost(_ input: GhostInput) -> Ghost { var ghost = extractGhostDefault(input) ghost.contextValue = (type: Value.self, value: input.component.value) return ghost } } + +internal protocol _AnyContextValueProvider { + var _contextValueType: any ContextValue.Type { get } + var _contextValue: any ContextValue { get } +} diff --git a/Sources/React/Image/Image.swift b/Sources/React/Image/Image.swift new file mode 100644 index 0000000..6b1a2e1 --- /dev/null +++ b/Sources/React/Image/Image.swift @@ -0,0 +1,5 @@ +public struct Image { + +} + + diff --git a/Sources/React/Renderer/Instance.swift b/Sources/React/Renderer/Instance.swift new file mode 100644 index 0000000..433e88b --- /dev/null +++ b/Sources/React/Renderer/Instance.swift @@ -0,0 +1,92 @@ +import SRTDOM + +internal final class Instance { + final class ListenerBridge { + init() {} + + var js: JSEventListener? + var swift: EventListener? + } + + init() { + + } + + weak var owner: VNode? + var hooks: [any _AnyHookWrapper]? + var dom: JSNode? + var attributes: Attributes = [:] + var listeners: [String: ListenerBridge] = [:] + var contextValueHolder: ContextValueHolder? + var isDirty: Bool = false + + func renderDOMAttributes( + attributes newAttributes: Attributes + ) throws { + let dom = try (self.dom?.asHTMLElement()).unwrap("dom.asHTMLElement") + let oldAttributes = self.attributes + + for name in self.attributes.keys { + if newAttributes[name] == nil { + try dom.removeAttribute(name) + } + } + + for (name, newValue) in newAttributes { + if newValue != oldAttributes[name] { + try dom.setAttribute(name, newValue) + } + } + + self.attributes = newAttributes + } + + func renderDOMListeners( + listeners newListeners: EventListeners + ) throws { + let dom = try (self.dom?.asHTMLElement()).unwrap("dom.asHTMLElement") + + // コピー必要なんだっけ? + for (type, bridge) in Array(self.listeners) { + if bridge.swift != newListeners[type] { + if let js = bridge.js { + try dom.removeEventListener(type, js) + } + self.listeners[type] = nil + } + } + + for (type, newListener) in newListeners { + if let bridge = self.listeners[type] { + if newListener == bridge.swift { + continue + } + + bridge.swift = newListener + } else { + let bridge = ListenerBridge() + self.listeners[type] = bridge + + let js = JSEventListener { [weak bridge] (event) in + guard let bridge, let swift = bridge.swift else { return } + swift(event) + } + + bridge.js = js + bridge.swift = newListener + + try dom.addEventListener(type, js) + } + } + } + + func markDirty() { + isDirty = true + } + + func consumeDirty() -> Bool { + defer { isDirty = false } + return isDirty + } + +} diff --git a/Sources/React/Renderer/ReactRoot.swift b/Sources/React/Renderer/ReactRoot.swift index 2af6901..1a7a349 100644 --- a/Sources/React/Renderer/ReactRoot.swift +++ b/Sources/React/Renderer/ReactRoot.swift @@ -35,8 +35,8 @@ public final class ReactRoot { switch action { case .renderRoot(let node): try runRenderRoot(node: node) - case .update(let node): - try runUpdate(node: node) + case .update(let instance): + try runUpdate(instance: instance) case .effect(let effect): effect.run() } @@ -54,8 +54,8 @@ public final class ReactRoot { scheduler.resume() } - private func scheduleUpdate(node: VNode) { - scheduler.schedule(action: .update(node)) + private func scheduleUpdate(instance: Instance) { + scheduler.schedule(action: .update(instance)) } private func scheduleEffect(_ effect: Effect.Task) { @@ -65,7 +65,7 @@ public final class ReactRoot { } private func runRenderRoot(node: Node) throws { - let newRoot = Self.makeVNode(component: Fragment()) + let newRoot = VNode(component: Fragment()) let newChildren = Self.makeChildren(node: node) newRoot.appendChildren(newChildren) let oldChildren = root?.children ?? [] @@ -78,8 +78,9 @@ public final class ReactRoot { self.root = newRoot } - private func runUpdate(node oldTree: VNode) throws { - let newTree = VNode(ghost: oldTree.ghost) + private func runUpdate(instance: Instance) throws { + let oldTree = try instance.owner.unwrap("instance.owner") + let newTree = VNode(component: oldTree.component) let parent = try oldTree.parent.unwrap("oldTree.parent") let index = try parent.index(of: oldTree).unwrap("oldTree index") @@ -94,16 +95,9 @@ public final class ReactRoot { } } - private static func makeVNode(component: C) -> VNode { - let ghost = C._extractGhost( - .init(component: component) - ) - return VNode(ghost: ghost) - } - private static func makeChildren(node: Node) -> [VNode] { let nodes = Nodes.normalize(node: node) - return nodes.map { makeVNode(component: $0) } + return nodes.map { VNode(component: $0) } } private func withLocation(_ location: JSNodeLocationRight?, _ body: () throws -> Void) rethrows { @@ -150,8 +144,8 @@ public final class ReactRoot { } private func domLocation(of node: VNode) throws -> JSNodeLocationRight? { - let parent: JSNode = try node.parentTagNode?.dom.unwrap("dom") ?? self.dom.asNode() - let prev: JSNode? = try node.prevSiblingTagNode?.dom.unwrap("dom") + let parent: JSNode = try node.parentTagNode?.instance?.dom.unwrap("dom") ?? self.dom.asNode() + let prev: JSNode? = try node.prevSiblingTagNode?.instance?.dom.unwrap("dom") return JSNodeLocationRight(parent: parent, prev: prev) } @@ -162,148 +156,28 @@ public final class ReactRoot { ) throws { var doesRenderChildren = true - if let oldTree { - oldTree.new = .some(newTree) - } - if let newTree { - if let newTag = newTree.tagElement { - let dom: JSHTMLElement = if let oldTree { - try oldTree.domTag.unwrap("oldTree.domTag") - } else { - try document.createElement(newTag.tagName) - } - - let oldTag = oldTree?.tagElement - newTree.dom = dom.asNode() - newTree.listeners = oldTree?.listeners ?? [:] - newTag.ref?.value = dom - - try renderDOMAttributes( - dom: dom, - newAttributes: newTag.attributes, - oldAttributes: oldTag?.attributes ?? [:] - ) - try renderDOMEventListeners( - node: newTree, - dom: dom, - newListeners: newTag.listeners, - oldListeners: oldTag?.listeners ?? [:] - ) - } else if let text = newTree.textElement { - let dom: JSText = try { - if let oldTree { - let dom = try oldTree.domText.unwrap("oldTree.domText") - dom.data = text.value - return dom - } else { - return try document.createTextNode(text.value) - } - }() - - newTree.dom = dom.asNode() - } - - if let location = currentLocation { - if let dom = newTree.dom { - if location != dom.locationRight { - try dom.remove() - try dom.insert(at: location) - } - - currentLocation?.prev = dom - } - } - - if let contextValue = newTree.ghost.contextValue { - let holder: ContextValueHolder = if let oldHolder = oldTree?.contextValueHolder, - oldHolder.type == contextValue.type - { - oldHolder - } else { - ContextValueHolder(type: contextValue.type) - } - - newTree.contextValueHolder = holder - holder.value = contextValue.value - } - - renderGhost(newTree: newTree, oldTree: oldTree) - - let updater = { [weak self, weak newTree] () -> Void in - guard let self, let newTree else { return } - newTree.markDirty() - self.scheduleUpdate(node: newTree) - } - - for context in newTree.ghost.contexts { - if let holder = contextValueHolders[ObjectIdentifier(context.valueType)] { - let dsp = holder.emitter.on(handler: updater) - context.setHolder(holder, disposable: dsp) - } else { - context.setHolder(nil, disposable: nil) - } - } - - for state in newTree.ghost.states { - state.setDidChange(updater) - } - - var isDirty = newTree.consumeDirty() - - if !isDirty { - if let newDeps = newTree.ghost.component.deps, - let oldDeps = oldTree?.ghost.component.deps, - newDeps == oldDeps - { - // same - } else { - isDirty = true - } - } + let oldInstance = oldTree?.instance + oldTree?.instance = nil - if isDirty { - let component = newTree.ghost.component + let instance = oldInstance ?? Instance() + newTree.instance = instance - willComponentRender?(component) - let node: Node = component.render() - didComponentRender?(component) + try renderDOM(tree: newTree, instance: instance) + try moveDOM(instance: instance) + try updateContextValue(tree: newTree, instance: instance) + prepareHooks(component: newTree.component, instance: instance) + subscribeHooks(instance: instance) - let newChildren = Self.makeChildren(node: node) - newTree.appendChildren(newChildren) - } else { - newTree.appendChildren(oldTree?.children ?? []) - doesRenderChildren = false - } + // short circuit + doesRenderChildren = instance.consumeDirty() || + isChanged(new: newTree, old: oldTree) } if doesRenderChildren { - try renderChildren( - new: newTree?.children ?? [], - old: oldTree?.children ?? [], - parent: newTree?.dom, - contextValueHolder: newTree?.contextValueHolder - ) - } else { - if let newTree, - newTree.dom == nil, - let _ = currentLocation - { - let doms = newTree.domChildren - if isMove { - for dom in doms { - try dom.remove() - } - for dom in doms { - try dom.insert(at: currentLocation!) - currentLocation!.prev = dom - } - } else { - if let dom = doms.last { - currentLocation!.prev = dom - } - } - } + try renderChildren(newTree: newTree, oldTree: oldTree) + } else if let newTree { + try skipRenderChildren(newTree: newTree, oldTree: oldTree, isMove: isMove) } if newTree == nil { @@ -312,7 +186,6 @@ public final class ReactRoot { } } - if let newTree { for effect in newTree.ghost.effects { let object = effect.effectObject @@ -331,21 +204,116 @@ public final class ReactRoot { } } - private func renderGhost(newTree: VNode, oldTree: VNode?) { - for (index, newHook) in newTree.ghost.hooks.enumerated() { - let oldHook = oldTree?.ghost.hooks[index] + private func renderDOM(tree: VNode, instance: Instance) throws { + switch tree.component { + case let newTag as HTMLElement: + let dom: JSHTMLElement = try { + if let dom = instance.dom?.asHTMLElement() { + return dom + } + + let dom = try document.createElement(newTag.tagName) + instance.dom = dom.asNode() + return dom + }() + + newTag.ref?.value = dom + + try instance.renderDOMAttributes(attributes: newTag.attributes) + try instance.renderDOMListeners(listeners: newTag.listeners) + case let text as TextElement: + if let dom = instance.dom?.asText() { + dom.data = text.value + return + } + let dom = try document.createTextNode(text.value) + instance.dom = dom.asNode() + default: break + } + } + + private func moveDOM(instance: Instance) throws { + guard let location = currentLocation else { return } + + if let dom = instance.dom { + if location != dom.locationRight { + try dom.remove() + try dom.insert(at: location) + } - newHook._prepareAny(object: oldHook?.object) + currentLocation?.prev = dom } } + private func updateContextValue(tree: VNode, instance: Instance) throws { + guard let provider = tree.component as? any _AnyContextValueProvider else { return } + + let type = provider._contextValueType + let value = provider._contextValue + + let holder: ContextValueHolder = { + if let holder = instance.contextValueHolder, + holder.type == type { return holder } + + let holder = ContextValueHolder(type: type) + instance.contextValueHolder = holder + return holder + }() + + holder.value = value + } + + private func prepareHooks(component: any Component, instance: Instance) { + let hooks = Components.extractHooks(component) + + if let oldHooks = instance.hooks { + for (new, old) in zip(hooks, oldHooks) { + new._prepareAny(object: old.object) + } + } else { + for hook in hooks { + hook._prepareAny(object: nil) + } + } + + instance.hooks = hooks + } + + private func subscribeHooks(instance: Instance) { + let updater = { [weak self, weak instance] () -> Void in + guard let self, let instance else { return } + instance.markDirty() + self.scheduleUpdate(instance: instance) + } + + for hook in instance.hooks ?? [] { + switch hook { + case let context as any _AnyContextHook: + if let holder = contextValueHolders[ObjectIdentifier(context.valueType)] { + let dsp = holder.emitter.on(handler: updater) + context.setHolder(holder, disposable: dsp) + } else { + context.setHolder(nil, disposable: nil) + } + case let state as any _AnyStateHook: + state.setDidChange(updater) + default: break + } + } + } + + private func isChanged(new: VNode, old: VNode?) -> Bool { + guard let newDeps = new.component.deps else { return false } + return newDeps != old?.component.deps + } + private func buildContextValueHolders(for node: VNode) -> [ObjectIdentifier: ContextValueHolder] { var result: [ObjectIdentifier: ContextValueHolder] = [:] // skip self var node: VNode? = node.parent while let n = node { - if let holder = n.contextValueHolder { + if let holder = n.instance?.contextValueHolder { let typeID = ObjectIdentifier(holder.type) if result[typeID] == nil { result[typeID] = holder @@ -358,6 +326,29 @@ public final class ReactRoot { return result } + private func renderChildren( + newTree: VNode?, + oldTree: VNode? + ) throws { + if let newTree { + let component = newTree.component + + willComponentRender?(component) + let node: Node = component.render() + didComponentRender?(component) + + let newChildren = Self.makeChildren(node: node) + newTree.appendChildren(newChildren) + } + + try renderChildren( + new: newTree?.children ?? [], + old: oldTree?.children ?? [], + parent: newTree?.instance?.dom, + contextValueHolder: newTree?.instance?.contextValueHolder + ) + } + private func renderChildren( new: [VNode], old: [VNode], @@ -415,59 +406,25 @@ public final class ReactRoot { nextIndex = newChildren.count } - private func renderDOMAttributes( - dom: JSHTMLElement, - newAttributes: Attributes, - oldAttributes: Attributes - ) throws { - for name in oldAttributes.keys { - if newAttributes[name] == nil { - try dom.removeAttribute(name) - } - } + private func skipRenderChildren(newTree: VNode, oldTree: VNode?, isMove: Bool) throws { + newTree.appendChildren(oldTree?.children ?? []) - for (name, newValue) in newAttributes { - if newValue != oldAttributes[name] { - try dom.setAttribute(name, newValue) - } - } - } + if let _ = newTree.instance?.dom { return } + guard let _ = currentLocation else { return } - private func renderDOMEventListeners( - node: VNode, - dom: JSHTMLElement, - newListeners: EventListeners, - oldListeners: EventListeners - ) throws { - for (type, oldListener) in oldListeners { - if oldListener != newListeners[type] { - if let bridge = node.listeners[type] { - if let js = bridge.js { - try dom.removeEventListener(type, js) - } - node.listeners[type] = nil - } - } - } - - for (type, newListener) in newListeners { - if newListener != oldListeners[type] { - if let bridge = node.listeners[type] { - bridge.swift = newListener - } else { - let bridge = VNode.ListenerBridge() - node.listeners[type] = bridge + let doms = newTree.domChildren - let js = JSEventListener { [weak bridge] (event) in - guard let bridge, let swift = bridge.swift else { return } - swift(event) - } - - bridge.js = js - bridge.swift = newListener - - try dom.addEventListener(type, js) - } + if isMove { + for dom in doms { + try dom.remove() + } + for dom in doms { + try dom.insert(at: currentLocation!) + currentLocation!.prev = dom + } + } else { + if let dom = doms.last { + currentLocation!.prev = dom } } } diff --git a/Sources/React/Renderer/Scheduler.swift b/Sources/React/Renderer/Scheduler.swift index b88973f..6c0528b 100644 --- a/Sources/React/Renderer/Scheduler.swift +++ b/Sources/React/Renderer/Scheduler.swift @@ -7,10 +7,10 @@ internal final class Scheduler { enum Action { case renderRoot(Node) - case update(VNode) + case update(Instance) case effect(Effect.Task) - var update: VNode? { + var update: Instance? { switch self { case .update(let x): x default: nil @@ -66,27 +66,13 @@ internal final class Scheduler { } updates.sort { (a, b) in - guard let a = a.update, let b = b.update else { return false } + guard let a = a.update?.owner, let b = b.update?.owner else { return false } return isLess(a, b) } actionQueue.replaceSubrange(.. Bool { a.equality == b.equality } @@ -56,15 +53,6 @@ package final class VNode: Hashable { hasher.combine(equality) } - public func markDirty() { - isDirty = true - } - - public func consumeDirty() -> Bool { - defer { isDirty = false } - return isDirty - } - public var parent: VNode? { _parent } public func removeFromParent() { @@ -123,23 +111,4 @@ package final class VNode: Hashable { public static func unknownNode(_ node: VNode) -> any Error { MessageError("unknown VNode: \(type(of: node))") } - - package static func tag( - _ name: String, - _ attributes: Attributes = [:], - _ children: [Node] = [] - ) -> VNode { - let tag = HTMLElement( - tagName: name, - attributes: attributes, - children: children - ) - return component(tag) - } - - package static func component(_ component: C) -> VNode { - let input = GhostInput(component: component) - let ghost = C._extractGhost(input) - return VNode(ghost: ghost) - } }