-
Notifications
You must be signed in to change notification settings - Fork 483
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #717 from Shopify/feature/remove-pull-from-reconciler
Remove pull from reconciler
- Loading branch information
Showing
14 changed files
with
397 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,193 @@ | ||
import type { RefObject } from "react"; | ||
|
||
import type { SkiaView } from "../views"; | ||
import type { SkiaValue } from "../values"; | ||
|
||
import { isSelector, isValue } from "./processors"; | ||
import type { Node } from "./nodes"; | ||
import type { AnimatedProps } from "./processors"; | ||
import { isSelector, isValue } from "./processors"; | ||
import { mapKeys } from "./typeddash"; | ||
|
||
type Unsubscribe = () => void; | ||
type Props = { [key: string]: unknown }; | ||
type Mutator = (value: unknown) => void; | ||
|
||
type SubscriptionState = { | ||
nodes: Map<Node, Mutator[]>; | ||
unsubscribe: null | Unsubscribe; | ||
}; | ||
|
||
export class DependencyManager { | ||
ref: RefObject<SkiaView>; | ||
subscriptions: Map< | ||
Node, | ||
{ values: SkiaValue<unknown>[]; unsubscribe: null | Unsubscribe } | ||
> = new Map(); | ||
|
||
constructor(ref: RefObject<SkiaView>) { | ||
this.ref = ref; | ||
} | ||
registerValues: (values: Array<SkiaValue<unknown>>) => () => void; | ||
subscriptions: Map<SkiaValue<unknown>, SubscriptionState> = new Map(); | ||
unregisterDependantValues: null | Unsubscribe = null; | ||
|
||
unSubscribeNode(node: Node) { | ||
const subscription = this.subscriptions.get(node); | ||
if (subscription && subscription.unsubscribe) { | ||
subscription.unsubscribe(); | ||
} | ||
this.subscriptions.delete(node); | ||
constructor( | ||
registerValues: (values: Array<SkiaValue<unknown>>) => () => void | ||
) { | ||
this.registerValues = registerValues; | ||
} | ||
|
||
subscribeNode(node: Node, props: Props) { | ||
const values = Object.values(props) | ||
.filter((v) => isValue(v) || isSelector(v)) | ||
.map((v) => (isSelector(v) ? v.value : (v as SkiaValue<unknown>))); | ||
/** | ||
* Call to unsubscribe all value listeners from the given node based | ||
* on the current list of subscriptions for the node. This function | ||
* is typically called when the node is unmounted or when one or more | ||
* properties have changed. | ||
* @param node Node to unsubscribe value listeners from | ||
*/ | ||
unsubscribeNode(node: Node) { | ||
const subscriptions = Array.from(this.subscriptions.values()).filter((p) => | ||
p.nodes.has(node) | ||
); | ||
|
||
if (values.length > 0) { | ||
this.subscriptions.set(node, { values, unsubscribe: null }); | ||
if (subscriptions) { | ||
subscriptions.forEach((si) => { | ||
// Delete node from subscription | ||
si.nodes.delete(node); | ||
|
||
// Remove subscription if there are no listeneres left on the value | ||
if (si.nodes.size === 0) { | ||
// There are no more nodes subscribing to this value, we can call | ||
// unsubscribe on it. | ||
if (!si.unsubscribe) { | ||
throw new Error("Failed to unsubscribe to value subscription"); | ||
} | ||
si.unsubscribe && si.unsubscribe(); | ||
|
||
// Remove from subscription states as well | ||
const element = Array.from(this.subscriptions.entries()).find( | ||
([_, sub]) => sub === si | ||
); | ||
if (!element) { | ||
throw new Error("Failed to find value subscription"); | ||
} | ||
if (!this.subscriptions.delete(element[0])) { | ||
throw new Error("Failed to delete value subscription"); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
subscribe() { | ||
if (this.ref.current === null) { | ||
throw new Error("Canvas ref is not set"); | ||
/** | ||
* Adds listeners to the provided values so that the node is notified | ||
* when a value changes. This is done in an optimized way so that this | ||
* class only needs to listen to the value once and then forwards the | ||
* change to the node and its listener. This method is typically called | ||
* when the node is mounted and when one or more props on the node changes. | ||
* @param node Node to subscribe to value changes for | ||
* @param props Node's properties | ||
* @param onResolveProp Callback when a property value changes | ||
*/ | ||
subscribeNode<P extends Record<string, unknown>>( | ||
node: Node, | ||
props: AnimatedProps<P> | ||
) { | ||
// Get mutators from node's properties | ||
const propSubscriptions = initializePropertySubscriptions(node, props); | ||
if (propSubscriptions.length === 0) { | ||
return; | ||
} | ||
this.subscriptions.forEach((subscription) => { | ||
if (subscription.unsubscribe === null) { | ||
subscription.unsubscribe = this.ref.current!.registerValues( | ||
subscription.values | ||
); | ||
|
||
// Install all mutators for the node | ||
propSubscriptions.forEach((ps) => { | ||
// Do we already have a state for this SkiaValue | ||
let subscriptionState = this.subscriptions.get(ps.value); | ||
if (!subscriptionState) { | ||
// Let's create a new subscription state for the skia value | ||
subscriptionState = { | ||
nodes: new Map(), | ||
unsubscribe: null, | ||
}; | ||
// Add single subscription to the new value | ||
subscriptionState.unsubscribe = ps.value.addListener((v) => { | ||
subscriptionState!.nodes.forEach((mutators) => | ||
mutators.forEach((m) => m(v)) | ||
); | ||
}); | ||
this.subscriptions.set(ps.value, subscriptionState); | ||
} | ||
// subscription mutators | ||
subscriptionState.nodes.set( | ||
node, | ||
propSubscriptions | ||
.filter((m) => m.value === ps.value) | ||
.map((m) => m.mutator) | ||
); | ||
}); | ||
} | ||
|
||
unsubscribe() { | ||
this.subscriptions.forEach(({ unsubscribe }) => { | ||
if (unsubscribe) { | ||
unsubscribe(); | ||
} | ||
/** | ||
* Called when the hosting container is mounted or updated. This ensures that we have | ||
* a ref to the underlying SkiaView so that we can registers redraw listeners | ||
* on values used in the current View automatically. | ||
*/ | ||
update() { | ||
// Remove any previous registrations | ||
if (this.unregisterDependantValues) { | ||
this.unregisterDependantValues(); | ||
} | ||
|
||
// Register redraw requests on the SkiaView for each unique value | ||
this.unregisterDependantValues = this.registerValues( | ||
Array.from(this.subscriptions.keys()) | ||
); | ||
} | ||
|
||
/** | ||
* Called when the hosting container is unmounted or recreated. This ensures that we remove | ||
* all subscriptions to Skia values so that we don't have any listeners left after | ||
* the component is removed. | ||
*/ | ||
remove() { | ||
// 1) Unregister redraw requests | ||
if (this.unregisterDependantValues) { | ||
this.unregisterDependantValues(); | ||
this.unregisterDependantValues = null; | ||
} | ||
|
||
// 2) Unregister nodes | ||
Array.from(this.subscriptions.values()).forEach((si) => { | ||
Array.from(si.nodes.keys()).forEach((node) => this.unsubscribeNode(node)); | ||
}); | ||
|
||
// 3) Clear the rest of the subscriptions | ||
this.subscriptions.clear(); | ||
} | ||
} | ||
|
||
const initializePropertySubscriptions = <P,>( | ||
node: Node<P>, | ||
props: AnimatedProps<P> | ||
) => { | ||
const nodePropSubscriptions: Array<{ | ||
value: SkiaValue<unknown>; | ||
mutator: Mutator; | ||
}> = []; | ||
|
||
mapKeys(props).forEach((key) => { | ||
const propvalue = props[key]; | ||
|
||
if (isValue(propvalue)) { | ||
// Subscribe to changes | ||
nodePropSubscriptions.push({ | ||
value: propvalue, | ||
mutator: (v) => (node.resolvedProps[key] = v as P[typeof key]), | ||
}); | ||
// Set initial value | ||
node.resolvedProps[key] = (propvalue as SkiaValue<P[typeof key]>).current; | ||
} else if (isSelector(propvalue)) { | ||
// Subscribe to changes | ||
nodePropSubscriptions.push({ | ||
value: propvalue.value, | ||
mutator: (v) => | ||
(node.resolvedProps[key] = propvalue.selector(v) as P[typeof key]), | ||
}); | ||
// Set initial value | ||
node.resolvedProps[key] = propvalue.selector( | ||
propvalue.value.current | ||
) as P[typeof key]; | ||
} else { | ||
// Set initial value | ||
node.resolvedProps[key] = propvalue as P[typeof key]; | ||
} | ||
}); | ||
|
||
return nodePropSubscriptions; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.