mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update reactive scoping docs
This commit is contained in:
parent
c68d0a6c18
commit
71bbaa2092
3 changed files with 189 additions and 57 deletions
|
|
@ -3,11 +3,34 @@
|
||||||
This is a brief document designed to give the user more insight into how Vide's
|
This is a brief document designed to give the user more insight into how Vide's
|
||||||
reactive system works.
|
reactive system works.
|
||||||
|
|
||||||
Vide's reactivity can be pictured as a graph, where each source, derived source,
|
## Graph Basics
|
||||||
and effect is a node on that graph. For example:
|
|
||||||
|
Vide's reactivity can be represented as a graph, where each source, derived
|
||||||
|
source, and effect is a node on that graph. The term "*reactive scope*" is just
|
||||||
|
an abstraction used to refer to these nodes. Each node is a reactive scope.
|
||||||
|
|
||||||
|
Each node stores a cached value, a side-effect function, cleanup functions,
|
||||||
|
its parents and children, and its owner and owned.
|
||||||
|
|
||||||
|
Whenever a node is updated it will:
|
||||||
|
|
||||||
|
1. destroy its owned nodes
|
||||||
|
2. run its cleanups
|
||||||
|
3. rerun its side-effect and update its cached value
|
||||||
|
4. if its cached value changes, update its children recursively.
|
||||||
|
|
||||||
|
There is a difference between children nodes and owned nodes:
|
||||||
|
|
||||||
|
- children nodes are updated when a parent is updated.
|
||||||
|
- owned nodes are destroyed when a parent is updated.
|
||||||
|
- both children and owned are destroyed when a parent is destroyed.
|
||||||
|
|
||||||
|
Nodes created by `root()` generally have no children, and only tracks owned.
|
||||||
|
Nodes created by `derive()` generally have no owned, and only tracks children.
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
||||||
root(function()
|
root(function()
|
||||||
local forename = source "quan"
|
local forename = source "quan"
|
||||||
local surname = source "xi"
|
local surname = source "xi"
|
||||||
|
|
@ -22,11 +45,6 @@ root(function()
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
Each time you create and derive sources, a new node representing that source is
|
|
||||||
created and added to the reactive graph. Each node stores a value and a
|
|
||||||
side-effect function. Each node also keeps track of its parents and children,
|
|
||||||
as well as any cleanups registered.
|
|
||||||
|
|
||||||
This code will produce a graph that looks like so:
|
This code will produce a graph that looks like so:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|
@ -49,32 +67,35 @@ flowchart
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Nodes connected by arrows represent parent and children connections.
|
||||||
|
Nodes within other nodes represent owner and owned connections.
|
||||||
|
|
||||||
Any time a node is updated, Vide will traverse and update that node's children,
|
Any time a node is updated, Vide will traverse and update that node's children,
|
||||||
its children's children, etc, until all nodes descending from that node has been
|
its children's children, etc, until all nodes descending from that node has been
|
||||||
updated. Traversal will stop at a node if that node's cached value does not
|
updated. Traversal will stop at a node if that node's cached value does not
|
||||||
change after an update.
|
change after an update.
|
||||||
|
|
||||||
For every node that is updated, a scope is opened for that node. These scopes
|
When the side-effect for a node is being reran when a node is updated, any
|
||||||
are referred to as "reactive scopes". Any source read from within a node's scope
|
other nodes read within that side-effect are set as parents of the node
|
||||||
will that node as a child. This is similar to cleanups, anytime a cleanup is
|
currently being reran. As those nodes are read, we know that the current node
|
||||||
registered, it is added to the node of the currently active scope.
|
depends on them, so any time those nodes are updated, they will update dependent
|
||||||
|
nodes since they will be stored as children.
|
||||||
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
|
|
||||||
active reactive scope is the node at the top of this stack.
|
|
||||||
|
|
||||||
When destroying a node, its descendents are traversed and also destroyed.
|
When destroying a node, its descendents are traversed and also destroyed.
|
||||||
When being destroyed, a node's connections (parents and children) are cleared,
|
When being destroyed, a node's connections (parents and children, owner and
|
||||||
and any pending cleanup functions are ran.
|
owned) are cleared, and any pending cleanup functions are ran.
|
||||||
|
|
||||||
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
||||||
the root node which will track any node created or derived inside its scope, or
|
the root node which will track any node created inside its scope, or any
|
||||||
any cleanups registered. Without it, nodes could be garbage collected without a
|
cleanups registered. Without it, nodes could be garbage collected without a
|
||||||
chance to run pending cleanups which can cause memory leakage.
|
chance to run pending cleanups which can cause memory leakage.
|
||||||
|
|
||||||
Nodes created by `source()` can actually exist outside of root nodes, since
|
Nodes created by `source()` can actually exist outside of root nodes, since
|
||||||
they do not have direct side-effects or cleanups, they do not have to be
|
they do not have direct side-effects or cleanups, they do not have to be
|
||||||
explicitly destroyed.
|
explicitly destroyed.
|
||||||
|
|
||||||
|
## Control-flow Graph Example
|
||||||
|
|
||||||
Control flow functions in Vide are special, as they can dynamically create and
|
Control flow functions in Vide are special, as they can dynamically create and
|
||||||
destroy new root scopes.
|
destroy new root scopes.
|
||||||
|
|
||||||
|
|
@ -120,21 +141,21 @@ This code produces a graph like so:
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#161618"
|
"tertiaryBorderColor": "#fff"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph root
|
subgraph root
|
||||||
counters --> indexes
|
counters --> indexes
|
||||||
end
|
|
||||||
|
|
||||||
subgraph root1[subroot 1]
|
subgraph root1[subroot 1]
|
||||||
n1[name] --> p1[prop binding]
|
n1[name] --> p1[prop binding]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph root2[subroot 2]
|
subgraph root2[subroot 2]
|
||||||
n2[name] --> p2[prop binding]
|
n2[name] --> p2[prop binding]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
indexes .-> root1 & root2
|
indexes .-> root1 & root2
|
||||||
|
|
@ -153,3 +174,79 @@ applies to all other control flow functions.
|
||||||
Whenever the root reactive scope is destroyed, all its children, `counters` and
|
Whenever the root reactive scope is destroyed, all its children, `counters` and
|
||||||
`indexes` will be destroyed too, which means that `indexes` children, the
|
`indexes` will be destroyed too, which means that `indexes` children, the
|
||||||
subroots, will also be destroyed. Everything is nicely cleaned up.
|
subroots, will also be destroyed. Everything is nicely cleaned up.
|
||||||
|
|
||||||
|
## Custom Control-flow Example
|
||||||
|
|
||||||
|
Below is a simple example of the `show()` control-flow function.
|
||||||
|
|
||||||
|
Each time `visible` changes, `show()` will destroy the current reactive scope
|
||||||
|
and rerun its function in a new one.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local visible = source(true)
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
show(visible, function()
|
||||||
|
return create "TextLabel" { Text = count }
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code produces a graph like so:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#fff"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart LR
|
||||||
|
subgraph root
|
||||||
|
show
|
||||||
|
|
||||||
|
subgraph subroot[show subroot]
|
||||||
|
p1[prop binding]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
visible --> show
|
||||||
|
count --> p1
|
||||||
|
show .-> subroot
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be recreated without the `show()` control-flow function, with the
|
||||||
|
following code:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local visible = source(true)
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
local output = derive(function()
|
||||||
|
visible()
|
||||||
|
|
||||||
|
-- untrack so any source read from within this scope
|
||||||
|
-- will not cause the outer `derive()` call to rerun,
|
||||||
|
-- we only want `derive()` to rerun when `visible` changes
|
||||||
|
return untrack(function()
|
||||||
|
local label = create "TextLabel" {}
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
label.Text = count()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return label
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the above code samples will produce the same visible result.
|
||||||
|
|
|
||||||
|
|
@ -981,19 +981,76 @@ TEST("create()", wrap_root(function()
|
||||||
end))
|
end))
|
||||||
|
|
||||||
TEST("show()", wrap_root(function()
|
TEST("show()", wrap_root(function()
|
||||||
-- uses switch() internally, more extensive testing of scoping not needed
|
local untrack = vide.untrack
|
||||||
|
local cleanup = vide.cleanup
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
local show = vide.show
|
local show = vide.show
|
||||||
|
local root = vide.root
|
||||||
|
|
||||||
local value = source("truey" :: unknown)
|
do CASE "main"
|
||||||
local function one() return 1 end
|
-- uses switch() internally, more extensive testing of scoping not needed
|
||||||
local function two() return 2 end
|
local value = source("truey" :: unknown)
|
||||||
|
local function one() return 1 end
|
||||||
|
local function two() return 2 end
|
||||||
|
|
||||||
local output = show(value, one, two)
|
local output = show(value, one, two)
|
||||||
|
|
||||||
CHECK(output() == 1)
|
CHECK(output() == 1)
|
||||||
value(nil)
|
value(nil)
|
||||||
CHECK(output() == 2)
|
CHECK(output() == 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "alt"
|
||||||
|
local visible = vide.source(true)
|
||||||
|
local count = vide.source(0)
|
||||||
|
|
||||||
|
local outer = 0
|
||||||
|
local inner = 0
|
||||||
|
local destroyed = 0
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
effect(function()
|
||||||
|
visible()
|
||||||
|
outer += 1
|
||||||
|
|
||||||
|
untrack(function()
|
||||||
|
effect(function()
|
||||||
|
count()
|
||||||
|
|
||||||
|
inner += 1
|
||||||
|
|
||||||
|
cleanup(function()
|
||||||
|
destroyed += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(outer == 1)
|
||||||
|
CHECK(inner == 1)
|
||||||
|
CHECK(destroyed == 0)
|
||||||
|
|
||||||
|
count(count() + 1)
|
||||||
|
CHECK(outer == 1)
|
||||||
|
CHECK(inner == 2)
|
||||||
|
CHECK(destroyed == 1)
|
||||||
|
|
||||||
|
visible(false)
|
||||||
|
CHECK(outer == 2)
|
||||||
|
CHECK(inner == 3)
|
||||||
|
CHECK(destroyed == 2)
|
||||||
|
|
||||||
|
count(count() + 1)
|
||||||
|
CHECK(outer == 2)
|
||||||
|
CHECK(inner == 4)
|
||||||
|
CHECK(destroyed == 3)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
TEST("switch()", wrap_root(function()
|
TEST("switch()", wrap_root(function()
|
||||||
|
|
|
||||||
24
todo.md
24
todo.md
|
|
@ -9,26 +9,4 @@
|
||||||
- batch
|
- batch
|
||||||
- optimize `indexes()` double-diffing
|
- optimize `indexes()` double-diffing
|
||||||
- improve crash course, some sections feel like information dumps
|
- improve crash course, some sections feel like information dumps
|
||||||
- reactive scopes
|
- cleanup source and tests
|
||||||
- define behavior of creating a reactive scope within a non-root reactive scope
|
|
||||||
- i.e. an effect within an effect
|
|
||||||
- error on attempt for now
|
|
||||||
- define behavior of destruction of a node with a child, where the node and
|
|
||||||
its child are in separate, non-nested root reactive scopes.
|
|
||||||
- should destruction of the parent also destroy the child?
|
|
||||||
- or should destruction of the parent silently disconnect the child, without
|
|
||||||
invoking the child's cleanups?
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
|
|
||||||
subgraph root1
|
|
||||||
parent
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph root2
|
|
||||||
child
|
|
||||||
end
|
|
||||||
|
|
||||||
parent --> child
|
|
||||||
```
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue