Refine reconciler prototype code

This commit is contained in:
Max Desiatov 2018-11-29 09:29:01 +00:00
parent 093c0b6881
commit 60e7062d66
No known key found for this signature in database
GPG Key ID: FE08EBF9CF58CBA2
7 changed files with 218 additions and 147 deletions

View File

@ -6,7 +6,7 @@
//
struct AnyEquatable: Equatable {
public struct AnyEquatable: Equatable {
let value: Any
private let equals: (Any) -> Bool

View File

@ -0,0 +1,30 @@
//
// Diffable.swift
// Gluon
//
// Created by Max Desiatov on 28/11/2018.
//
protocol Diffable {
func diffKeyPaths(other: Self) -> [PartialKeyPath<Self>]
}
struct Props: Equatable, Diffable {
func diffKeyPaths(other: Props) -> [PartialKeyPath<Props>] {
var result = [PartialKeyPath<Props>]()
if height != other.height {
result.append(\Props.height)
}
if width != other.width {
result.append(\Props.width)
}
return result
}
var height: Double
var width: Float
static let kp = [\Props.height, \Props.width]
}

View File

@ -5,70 +5,30 @@
// Created by Max Desiatov on 06/11/2018.
//
protocol Diffable {
func diffKeyPaths(other: Self) -> [PartialKeyPath<Self>]
}
protocol BaseComponent: Equatable {
}
public struct Hooks {
var currentReconciler: StackReconciler?
var currentComponent: MountedCompositeComponent?
public struct Component<P: Equatable>: Equatable {
let id: String
let base: (P) -> Node
/// The generated uuid might incur some app start up time penalty. It would
/// be great if there was a nice way to generate a unique static string.
/// One approach could be using `"\(#file)\(#line)"` as a default value,
/// but that leaks absolute filepaths, which is not ideal from a security
/// perspective.
public init(id: String = UUID().uuidString, base: @escaping (P) -> Node) {
self.id = id
self.base = base
}
public static func ==(lhs: Component<P>, rhs: Component<P>) -> Bool {
return lhs.id == rhs.id
}
}
public struct Node {
let component: Any
let props: Any
/// Closure with a component rerendered with new props
let rerenderIfNeeded: (Node) -> Node?
public init<P: Equatable>(_ component: Component<P>, _ props: P) {
self.component = component
self.props = props
rerenderIfNeeded = {
guard let newComponent = $0.component as? Component<P>,
newComponent == component,
let newProps = $0.props as? P,
newProps != props else { return nil }
return component.base(newProps)
}
}
}
struct Props: Equatable, Diffable {
func diffKeyPaths(other: Props) -> [PartialKeyPath<Props>] {
var result = [PartialKeyPath<Props>]()
if height != other.height {
result.append(\Props.height)
public func state<T>(_ initial: T,
id: Int = #line) -> (T, (T) -> ()) {
guard let component = currentComponent,
let reconciler = currentReconciler else {
fatalError("""
attempt to use `state` hook outside of a `render` function,
or `render` is not called from a renderer
""")
}
if width != other.width {
result.append(\Props.width)
}
let current = currentComponent?.state[id] as? T ?? initial
return result
return (current, { [weak reconciler, weak component] new in
// avoiding an indirect reference cycle here: this closure can be
// owned by callbacks owned by node's target, which is strongly referenced
// from node. Same with the reconciler.
guard let component = component,
let reconciler = reconciler else { return }
reconciler.queue(state: new, for: component, id: id) })
}
var height: Double
var width: Float
static let kp = [\Props.height, \Props.width]
}

View File

@ -0,0 +1,49 @@
//
// MountedComponent.swift
// Gluon
//
// Created by Max Desiatov on 28/11/2018.
//
class MountedComponent {
let key: String?
let props: AnyEquatable
let children: AnyEquatable
fileprivate init(key: String?, props: AnyEquatable, children: AnyEquatable) {
self.key = key
self.props = props
self.children = children
}
static func make(_ node: Node) -> MountedComponent {
switch node.type {
case let .composite(type):
return MountedCompositeComponent(node, type)
case let .base(type):
return MountedBaseComponent(node, type)
}
}
}
final class MountedCompositeComponent: MountedComponent {
let type: AnyCompositeComponent.Type
var state = [Int: Any]()
fileprivate init(_ node: Node, _ type: AnyCompositeComponent.Type) {
self.type = type
super.init(key: node.key, props: node.props, children: node.children)
}
}
final class MountedBaseComponent: MountedComponent {
let type: AnyBaseComponent.Type
var target: Any?
fileprivate init(_ node: Node, _ type: AnyBaseComponent.Type) {
self.type = type
super.init(key: node.key, props: node.props, children: node.children)
}
}

View File

@ -5,51 +5,76 @@
// Created by Max Desiatov on 28/11/2018.
//
private let _hooks = Hooks()
import Dispatch
extension Component {
// TODO: this won't work in multi-threaded scenarios
private var _hooks = Hooks()
extension CompositeComponent {
public static var hooks: Hooks {
return _hooks
}
}
public struct Hooks {
public func state<T>(_ initial: T,
id: Int = #line) -> (T, (T) -> ()) {
return (initial, { _ in })
}
}
struct Pair<T: Hashable, U: Hashable>: Hashable {
let first: T
let second: U
}
final class NodeReference {
let key: String?
let props: AnyEquatable
let children: AnyEquatable
let type: _Component.Type
init(node: Node) {
self.key = node.key
self.props = node.props
self.children = node.children
self.type = node.type
}
}
final class StackReconciler {
/// A map from a fully qualified component type name and its state hook id
/// to a current state value.
var state = [Pair<String, Int>: Any]()
private var queuedState = [(MountedCompositeComponent, Int, Any)]()
let root: NodeReference
private let rootComponent: MountedComponent
private let rootTarget: Any
private weak var renderer: Renderer!
func reconcile(node reference: NodeReference, with node: Node) {
init(node: Node, target: Any, renderer: Renderer) {
self.renderer = renderer
rootTarget = target
rootComponent = MountedComponent.make(node)
if let component = rootComponent as? MountedBaseComponent,
let type = component.type as? RendererBaseComponent.Type {
component.target = renderer.mountTarget(to: target, with: type)
}
}
init(root: Node) {
self.root = NodeReference(node: root)
func queue(state: Any, for node: MountedCompositeComponent, id: Int) {
let scheduleReconcile = queuedState.isEmpty
queuedState.append((node, id, state))
guard scheduleReconcile else { return }
DispatchQueue.main.async {
self.updateStateAndReconcile()
}
}
private func render(composite type: AnyCompositeComponent.Type,
component: MountedCompositeComponent) -> Node? {
_hooks.currentReconciler = self
_hooks.currentComponent = component
let result = type.render(props: component.props,
children: component.children)
_hooks.currentComponent = nil
_hooks.currentReconciler = nil
return result
}
private func updateStateAndReconcile() {
for (component, id, state) in queuedState {
guard let renderedNode = render(composite: component.type,
component: component) else {
assertionFailure("""
state update scheduled for a component that's not composite or
has props and children types that don't match
""")
continue
}
component.state[id] = state
reconcile(node: component, with: renderedNode)
}
}
private func reconcile(node reference: MountedComponent, with node: Node) {
}
}

View File

@ -14,19 +14,15 @@ extension Never: Equatable {
}
}
/// Type-erased version of Component to work around
/// the lack of generalized existentials in Swift
public protocol _Component {
}
/// Conforming to this protocol, but implementing support for these new types
/// in a renderer would make that renderer skip unknown types of children.
public protocol ChildrenType {
}
/// You should never directly conform to this protocol, use `HostComponent`
/// for host components and `Component` for composite components.
public protocol BaseComponent: _Component {
public protocol AnyBaseComponent {
}
public protocol BaseComponent: AnyBaseComponent {
associatedtype Props: Equatable
associatedtype Children: ChildrenType & Equatable
@ -34,14 +30,32 @@ public protocol BaseComponent: _Component {
var props: Props { get }
}
public protocol Component: _Component {
public protocol AnyCompositeComponent {
static func render(props: AnyEquatable, children: AnyEquatable) -> Node?
}
public protocol CompositeComponent: AnyCompositeComponent {
associatedtype Props: Equatable
associatedtype Children: Equatable
static func render(props: Props, children: Children) -> Node
}
public protocol LeafComponent: Component where Children == Never {
public extension CompositeComponent {
static func render(props: AnyEquatable, children: AnyEquatable) -> Node? {
guard let props = props as? Props,
let children = children as? Children else {
assertionFailure("""
incorrect types of `props` and `children` arguments passed to
`AnyComponent.render`
""")
return nil
}
return render(props: props, children: children)
}
}
public protocol LeafComponent: CompositeComponent where Children == Never {
static func render(props: Props) -> Node
}
@ -51,6 +65,34 @@ public extension LeafComponent {
}
}
enum ComponentType: Equatable {
static func == (lhs: ComponentType, rhs: ComponentType) -> Bool {
switch (lhs, rhs) {
case let (.base(ltype), .base(rtype)):
return ltype == rtype
case let (.composite(ltype), .composite(rtype)):
return ltype == rtype
default:
return false
}
}
case base(AnyBaseComponent.Type)
case composite(AnyCompositeComponent.Type)
var composite: AnyCompositeComponent.Type? {
guard case let .composite(type) = self else { return nil }
return type
}
var base: AnyBaseComponent.Type? {
guard case let .base(type) = self else { return nil }
return type
}
}
public struct Node: Equatable {
/// Equatable can't be automatically derived for `type` property?
public static func == (lhs: Node, rhs: Node) -> Bool {
@ -63,7 +105,7 @@ public struct Node: Equatable {
let key: String?
let props: AnyEquatable
let children: AnyEquatable
let type: _Component.Type
let type: ComponentType
}
extension BaseComponent {
@ -73,7 +115,7 @@ extension BaseComponent {
return Node(key: key,
props: AnyEquatable(props),
children: AnyEquatable(children),
type: self.self)
type: .base(self))
}
}
@ -125,41 +167,6 @@ public struct StackView: BaseComponent {
public let children: [Node]
}
// MARK: Legacy
private protocol ComponentType: BaseComponentType {
associatedtype Props: Equatable
var props: Props { get }
init(props: Props, children: [Node])
}
private protocol BaseComponentType {
var children: [Node] { get }
init?(props: AnyEquatable, children: [Node])
}
private protocol StatefulComponent: ComponentType {
associatedtype State: Default
var state: State { get }
init(props: Props, state: State, children: [Node])
}
extension StatefulComponent {
init(props: Props, children: [Node]) {
self.init(props: props, state: State(), children: children)
}
}
extension StatefulComponent {
func setState(setter: (inout State) -> ()) {
}
}
// well, this gets problematic:
// 1. `props` needs to be `var` for renderer to update them from node updates,
// but this means `StatefulComponent` implementor is compelled to modify

View File

@ -7,10 +7,10 @@
import Foundation
protocol Renderer {
func mountTarget(to parent: Any, with component: RendererBaseComponent) -> Any
func update(target: Any, with component: RendererBaseComponent)
func umount(target: Any, from parent: Any, with component: RendererBaseComponent)
protocol Renderer: class {
func mountTarget(to parent: Any, with component: RendererBaseComponent.Type) -> Any
func update(target: Any, with component: RendererBaseComponent.Type)
func umount(target: Any, from parent: Any, with component: RendererBaseComponent.Type)
}
protocol RendererBaseComponent {