feat: architecture document and edit list

This commit is contained in:
Jonathan Kelley committed 2021-07-14 17:04:58 -04:00
1 parent 211023b92d
commit e7238762ae
9 files changed
+344 -340

No files matched your search

+80 -43
View File
@@ -1,64 +1,101 @@
# This module includes all life-cycle related mechanics, including the virtual DOM, scopes, properties, and lifecycles.
# Dioxus Core Architecture:
---
Main topics covered here:
- Fiber, Concurrency, and Cooperative Scheduling
- Suspense
- Signals
- Patches
- Diffing
- Const/Static structures
- Components/Scope
- Hooks
- VNode Bump Arenas
The VirtualDom is designed as so:
## Components/Scope
VDOM contains:
All components in Dioxus are backed by something called the `Scope`. As a user, you will never directly interact with the `Scope` as most calls are shuttled through `Context`. Scopes manage all the internal state for components including hooks, bump arenas, and (unsafe!) lifetime management.
- An arena of component scopes.
- A scope contains
- lifecycle data
- hook data
- Event queue
- An event
Whenever a new component is created from within a user's component, a new "caller" closure is created that captures the component's properties. In contrast to Yew, we allow components to borrow from their parents, provided their `memo` (essentially canComponentUpdate) method returns false for non-static items. The `Props` macro figures this out automatically, and implements the `Properties` trait with the correct `canComponentUpdate` flag. Implementing this method manually is unsafe! With the `Props` macro you can manually disable memoization, but cannot manually enable memoization for non 'static properties structs without invoking unsafe. For 99% of cases, this is fine.
A VDOM is
During diffing, the "caller" closure is updated if the props are not `static. This is very important! If the props change, the old version will be referencing stale data that exists in an "old" bump frame. However, if we cycled the bump frames twice without updating the closure, then the props will point to invalid data and cause memory safety issues. Therefore, it's an invariant to ensure that non-'static Props are always updated during diffing.
- constructed from anything that implements "component"
## Hooks
A "Component" is anything (normally functions) that can be ran with a context to produce VNodes
Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets.
- Must implement properties-builder trait which produces a properties builder
The Dioxus hook model uses a Bump arena where user's data lives.
A Context
Initializing hooks:
- The component is created
- The virtualdom heuristics engine pre-allocates a calculated set of memory for the bump arena
- The component is called through "run_scope"
- Each call to use_hook allocates new data for the hook, stores the raw pointer, and pushes the hook index forward
- Each call to use_hook also stores a function pointer fn() that will be used to clean up the hook
- Once the component is finished running, the hook index is reset and the bump is shrunk to shrunk to fit
- The final size of the bump is then used in the heuristics engine for future components.
- Is a consumable struct
- Made of references to properties
- Holds a reference (lockable) to the underlying scope
- Is partially thread-safe
Running hooks:
- Each time use_hook is called, the internal hook state is fetched as &mut T
- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies
- The hook counter is incremented
# How to interact with the real dom?
## idea: use only u32
Dropping hooks:
- When the hook is scheduled for deletion, the "drop" function is run for each hook
- (dropping hooks is basically a drop implementation, but can be customized even for primitives)
pros:
- allows for 4,294,967,295 nodes (enough)
- u32 is relatively small
- doesn't add type noise
- allows virtualdom to stay completely generic
## VNode Bump Arenas
cons:
- cost of querying individual nodes (about 7ns per node query for all sizes w/ nohasher)
- 2-3 ns query cost with slotmap
- old IDs need to be manually freed when subtrees are destroyed
- can be collected as garbage after every render
- loss of ids between renders........................
- each new render doesn't know which node the old one was connected to unless it is visited
- When are nodes _not_ visited during diffing?
- They are predetermined to be removed (a parent was probed)
- something with keys?
- I think all nodes must be visited between diffs
-
## idea: leak raw nodes and then reclaim them on drop
## Diffing
# Fiber/Concurrency
The entire diffing logic for Dioxus lives in one file (diff.rs). Diffing in Dioxus is hyper-optimized for the types of structures generated by the rsx! and html! macros.
Dioxus is designed to support partial rendering. Partial rendering means that not _every_ component will be rendered on every tick. If some components were diffed.
The diffing engine in Dioxus expects the RealDom
Any given component will only be rendered on a single thread, so data inside of components does not need to be send/sync.
To schedule a render outside of the main component, the `suspense` method is exposed. `Suspense` consumes a future (valid for `bump) lifetime
## Patches
Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries.
There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself.
## Fiber/Concurrency and Cooperative Scheduling
When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work.
While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending.
Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom.
All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks.
```js
// returns a "deadline" object
function idle() {
return new Promise(resolve => requestIdleCallback(resolve));
}
```
## Suspense
In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier.
In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized.
Internally, the flow of suspense works like this:
1. accept the user's future. the future must be owned.
2. wrap that owned future with a new future that returns an EventTrigger
3. submit the future to the VirtualDOM's task queue
4. Poll the task queue in the VirtualDOM's event loop
5. use the EventTrigger from the future to find the use_suspense hook again
6. run that hook's callback with the component's bump arena and result of the future
7. set the hook's "inner value" with the completed valued so futures calls can resolve instantly
8. load the original placeholder node (either a suspended node or an actual node)
9. diff that node with the new node with a low priority on its own fiber
10. return the patches back to the event loop
11. apply the patches to the real dom