Reactive Change Detection
Jin
Posted on January 27, 2024
How can runtime know about changes?
π Polling: Periodic reconciliation
π Events: Occurrence of an event
π€ Links: List of subscribers
π Polling
States store only values and thatβs it. Runtime periodically checks the current value with the previous one. And if they differ, it triggers reactions.
// sometimes
if( state !== state_prev ) reactions()
This is how Angular, Svelte, and React work, for example. The problem with this approach is that for every sneeze, a lot of work is done, only to find out that almost nothing has changed.
It may seem to you that ordinary comparison is a trivial operation. And this is true in synthetic benchmarks. But in reality, states are scattered across memory, which results in mediocre use of processor caches. And the cherry on the cake is that such reconciliations have to be performed after each reaction in order to find out what exactly they changed in the state.
π Events
Each state additionally stores a list of change handler functions. Every time the state changes, all subscribers are called.
// on change
for( const reaction of this.reactions ) {
reaction()
}
This can be initiated manually, through a setter or a proxy. But in any case, the state knows nothing more about neighboring states, and the interaction is always one-way. This greatly limits the possible optimization algorithms. It also complicates debugging, because to find out who depends on whom in what way is a whole quest.
And the saddest thing is that storing an array of closures eats up a lot of memory. And nothing can be done about it.
π€ Links
States store direct references to each other, forming a global graph. Arrays of links are relatively memory-efficient, because each link is only 4-8 bytes. To communicate with neighbors, you just need to run through the array and pull the desired method from the neighboring state.
// on master change
for( const slave of this.slaves ) {
slave.obsolete()
}
// on slave complete
for( const master of this.masters ) {
master.finalize()
}
In the first example, you can see that when one state changes, we tell all dependents that they are out of date. And in the second, when the calculation of one state is completed, we tell all dependencies that the calculation is finished, and the caches that they could hold in case of repeated access can be freed. There can be many such interaction ways, which gives maximum flexibility in the supported algorithms.
In addition, when debugging, it is much easier to follow direct links between objects than to extract the necessary information from contexts captured by closures.
Posted on January 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.