why are watchables settable via underscore plus variable name in backdraftjs

32 Views Asked by At

Here's a simple backdraftjs component with watchables:

class TestComponent extends Component.withWatchables('veg') {
    constructor( kwargs ) {
        super( kwargs );
        
        // 'veg' is a watchable
        this._veg = 'carrot';
        console.log(this.veg);    // 'carrot'
        
        // 'fruit' is not a watchable
        this._fruit = 'banana';
        console.log(this.fruit);  // undefined
    }

    bdElements() {
        return e.div('hi there');
    }
}

Why am I allowed to set this.veg by setting this._veg? What's the purpose of that, and what's the difference if I just set it using this.veg = whatever?

1

There are 1 best solutions below

1
On

This is an intentional implementation technique employed by the backdraft framework. It follows the philosophy of trying to balance the equities of

  1. Simplicity
  2. Safety
  3. Capability to extend interfaces, particularly private interfaces.

The key ideas behind a watchable are three:

  1. Provide a standard getter/setter api (i.e., not a set/get function).
  2. Provide a signal when the variable mutates.
  3. Provide private before- and after- mutation hooks to affect variable mutation semantics.

All that said, a watchable variable must be actually stored somewhere. There are lots of options available to accomplish that...from closures to Proxy.

Proxy works great...if you have a browser that supports it. This version of Backdraft is now approaching 5 years old (and it original version is now more than 15 years old). Five years ago, Proxy wasn't available on many browsers important to commercial products; many businesses must still support browsers that do not support Proxy today.

Other techniques...closures, closures with maps, and so on...add a fair amount of complexity for questionable return; ergo, they were not selected.

So the technique chosen is trivial: for a watchable property x.

  1. Declare an actual property _x.
  2. Declare a getter for x that simply returns _x.
  3. Declare a setter for x that
    • Determines if there was actually a mutation (and said determination may be customized to be far more than ===); if not, then no-op, otherwise...
    • Applies a pre-mutate hook, if any; if pre-mutate hook cancels mutation, then no-op, otherwise...
    • Sets _x to mutated value
    • Applies a post-mutation hook, if any
    • Notifies all watchers

This fulfills the promise of a watchable.

It also exposes _x, and if the user of the class instance insists on breaking the abstraction barrier, then things will not work as expected (setting _x directly won't cause the normal watchable mutation machinery to fire). But the only way to do that is to explicitly write myInstance._x = someNewValue...which is pretty hard to do by accident. Finally note that this is no different...no more or less safe than breaking the private barrier in even strongly-typed languages like C++ by typecasting. Lastly, for the really unusual situation where the programmer really wants to mutate _x directly, it is possible.

I think all these things put together fairly balance the equities considered.