3 Reactivity caveats
Andrei Andreev edited this page 2023-07-16 09:48:20 +02:00

Q: What's the deal with the update functions? Why not use Vuex or directly dump the player object into data?

A: We did try that, and it backfired. When we did put the player object (PO) directly into vue data, Vue reactified every property (of every property of every property of every property, you get the idea) of the PO. Reactifying an object allows Vue to watch changing properties and only update the view if changes actually happen (assigning the same value results in no-op). The drawback is a (really small, but not in our case) overhead both in memory and performance for every property (of every property, you remember, right?). You can read more here: https://v2.vuejs.org/v2/guide/reactivity.html

Given that, PO is a really terrible target for reactifying for several reasons:

  1. PO is really huge - it contains dozens (hundreds even?) of properties that are only used in calculations and not in the view itself
  2. Our game is very dynamic, and we explicitly know that a lot of elements will update each frame. Reactivity in such case is redundant, and it leads to unnecessary overhead.
  3. Decimal. Decimal is an object with four properties (e, exponent, m, mantissa), and we have a shitton of them in PO, and each of them gets wrapped. But the worst part (Vue-wise, ofc) is that we use Decimal math everywhere, which results in triggering reactiveGetter everywhere a gazillion times each update. And the cherry on top is reassigning: reassigning reactive objects results in copying all dependent connections into a new object. The solution in the case of Decimal is to call fromDecimal (or better copyFrom, which has a more clear name), which copies e and m from the target, so it doesn't trigger additional reactivity.
  4. Our code is really shitty, and we have a lot of places where we access PO like player.x.y.z.f.s.d.g several times in a row without extracting this player.x.y.z.f.s.d.g into a separate variable. While in vanilla js it doesn't matter much to performance, it does so with reactive objects. Traversing a reactive object results (you guessed it) in triggering reactiveGetter on each step, which results in unnecessary overhead.