DEV Community

Cover image for Reactive Change Detection
Jin
Jin

Posted on • Originally published at page.hyoo.ru

Reactive Change Detection

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()
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)