Update reactive scoping docs

This commit is contained in:
Aaron Smith 2023-09-26 13:27:38 +01:00
parent c68d0a6c18
commit 71bbaa2092
3 changed files with 189 additions and 57 deletions

View file

@ -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.

View file

@ -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
View file

@ -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
```