State Mechanisms in JavaScript Frameworks

Mutable vs Immutable

Mutable

  • Ability to Change.
  • Maintains References.
const array = [2, 1, 4, -1, 5];

const sorted = array.sort();

console.log(sorted === array) // yep

Immutable

  • Values cannot change.
  • Improves performance (indirect)
const str = 'hello world';
const newStr = str.replace('he', 'a');

console.log(str === newStr); // Nope!

const array = [2, 1, 4, -1, 5];

function sort (arr) {
  return [...arr].sort();
}

const sorted = sort(array);

console.log(sorted === array); // Nope!

Immutable vs Mutable

  • Predicability: Unintended side-effects.
  • Performance: Memory.
  • Mutation Tracking: Detecting changes to re-render.
// Predicability.
const arr = [1, 2, 3];

arr.sort(); // unintended side-effect.
// Performance
function pushItem(arr, item) {
  let newArr = [...arr].push(item);
  
  return newArr;
}

// assume we do this 1000 times
const arr = [];
pushItem(arr, 1);

How Frameworks Do it

const [state, setState] = useState({});

setState({ a: 1 }); // Immutable.
this.state = { a: 1 };
this.state.a = 3; // Mutable

this.state.b = 4; // ??

Dependency Tracking vs. Dirty Checking

Dirty Checking

Check everything for anything that has changed

AngularJS: Doesn't track

Dirty Checking

Angular 2+ Doesn't dirty check, at least not all the time and not everything.

rEaCt?

Doesn't dirty check, but doesn't track either.

Dependency Tracking

Example on reactivity in Vue.js

<template>
  <div>
    <span>{{ displayName }}</span>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String
    }
  },
  data: () => ({
    firstName: '',
    lastName: ''
  }),
  computed: {
    // How does Vue know that any of these properties changed?
    displayName () {
      return `${this.title}. ${this.firstName} ${this.lastName}`;
    }
  }
};
</script>

Dependency Tracking

Reactivity in JavaScript

let x = 10;

function double () {
  return x * 2;
}

double(); // 20
x = 5;

We would need to keep calling 'double' to get the new value

ES5 getters

let x = 10;
let state = {};
Object.defineProperty(state, 'doubleX', {
  get () {
    return x * 2;
  }
})

state.doubleX; // 20
x = 5;
state.doubleX; // 10

Now we have a 'live' reference that returns the new value every time

This is not enough, we still need to 'get' the value.

We merely changed the style.

Watchers

// You can pass a function to the vm.$watch
// How does Vue know that it uses firstName and lastName?
this.$watch(() => {
  return `${this.firstName} ${this.lastName}`;
}, (val, oldVal) => {
  console.log(val, oldVal);
});

It is not magic, it is still JavaScript.

You can watch a function expression with Vue.js watch API

implementing watch()

1. We need to observe objects

const state = {
  x: 10
};

function observe (obj) {
  // ???
}

const observableState = observe(state);

2. We need to watch for changes

const double = () => {
  return state.x * 2;
};

function watch (exp, callback) {
  // ???
}

watch(double, val => {
  console.log(val);
});

observe()

// Takes an object and returns an observable one.
function observe (obj) {
  const observable = {};

  Object.keys(obj).forEach(key => {
    // Set the initial value under a private key.
    observable[`_${key}`] = { value: obj[key] };

    // Expose getters/setters to proxy the underlying value.
    Object.defineProperty(observable, key, {
      get () {
        return this[`_${key}`].value;
      },
      set (val) {
        this[`_${key}`].value = val;
      }
    });
  });

  return observable;
}

Something is still missing 🤔

watch()

function watch (expression, callback) {
  // Do something with the expression.

  // Run the callback once the expression dependencies change.
}

We hit a roadblock, just by executing the expression we have to be able to tell which properties did it use!

We can call these expressions: observers.

We need to "collect" those observers and have the subject notify them.

observe(): revised

// Simple function to wrap values in.
function wrap (value) {
  return {
    value,
    deps: [],
    notify () {
      // runs the observers
      this.deps.forEach(dep => dep());
    }
  };
}

We need to wrap values as subjects and have them notify their observers.

observe(): revised

// ...
Object.keys(obj).forEach(key => {
  // Set the initial value under a private key.
  observable[`_${key}`] = wrap(obj[key]);

  // Expose getters/setters to proxy the underlying value.
  Object.defineProperty(observable, key, {
    get () {
      const subject = this[`_${key}`];
      // collecting dependents
      subject.deps.push(
        // 😕 ???
      );

      return subject.value;
    },
    set (val) {
      const subject = this[`_${key}`];
      subject.value = val;
      // notify observers.
      subject.notify();
    }
  });
});
// ...

Collecting dependcies

The key is relying on an important but often ignored aspect of JavaScript ... 

JavaScript is single-threaded, which means there can only be 1 line of code running in any given moment...

By extension, There can only be 1 function running in any given moment.

🥁🥁🥁🥁

watch()

let activeFn;

// watches an expression and runs the callback.
function watch (expression, callback) {
  activeFn = expression;
 
  expression();
 
  activeFn = undefined;
}

Running Callbacks

// watches an expression and runs the callback.
function watch (expression, callback) {
  activeFn = expression;
  // not a very good way to do it, but 🤷‍
  expression.fn = callback;
  // by running the expression, we ran our `get` handlers.
  expression();
  activeFn = undefined; // must be done!
}

function wrap (value) {
  return {
    // ...
    notify () {
      // in our set handler
      obs.deps.forEach(dep => {
        const newVal = dep();

        if (dep.fn) {
          // pass the new value to the attached handler.
          dep.fn(newVal);
        }
      });
    }
  }
}

Rendering Demo

// state object
const state = observe({
  count: 0
});

// render-fn
const render = () => {
  return `
	<h1>
	  Count is: ${state.count}
	</h1>
  `;  
};

// start watching.
watch(render, (value) => {
  document.body.innerHTML = value;
});

// run the first render
document.body.innerHTML = render();

setInterval(() => {
  state.count++;
}, 500);

Rendering Demo

Simulated Reactivity vs Reactivity

Pull vs Push Systems

Pull Systems

 In Pull systems, the Consumer determines when it receives data from the data Producer. The Producer itself is unaware of when the data will be delivered to the Consumer.

Every JavaScript Function is a Pull system

Producer:

Pull systems:

 Passive

 (produces data when requested)

Consumer:

Active

 (decides when data is requested)

// Produce!
function doSomething () {
  // ...
}

// Consume!
doSomething();

Push Systems

In Push systems, the Producer determines when to send data to the Consumer. The Consumer is unaware of when it will receive that data.

Producer:

Push systems:

 Passive

 (produces data on its own)

Consumer:

Active

 (Reacts to produced data)

const el = document.querySelector('#el');

// Produce? Consume?
el.addEventListener(function () {
  // ...
});

Simulated Reactivity

react.js has nothing to do with reactivity as it doesn't react to anything.

 

Should name it `tell-me-when-to-render.js`

Reactivity In React

  • You use `setState` to tell it when something changes.
  • You use `shouldComponentUpdate` to tell it to skip unecessary re-renders.
  • You use `useMemo` or `useCallback` to cache/skip duplicate re-renders and you even supply the state so it can compare it.

These are called "performance hints"

Simulated Reactivity In Angular

Uses a concept called `zones` to wrap web APIs to simulate reactivity

function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        var changed = angular.runChangeDetection();
         if (changed) {
             angular.reRenderUIPart();
         }
     });
}

Angular wraps every almost Every API in JavaScript to simulate reactivity to then dirty check.

Then it only checks which template-referenced parts did change, it is efficient but requires template compilation.

Luckily with Angular when you need to fine-tune such performance bottlenecks if observed, you can change the detection strategy from pull to push using `onPush`

For Vue.js it knows what changed, what components depend on that change and thus is able to update each component independently without having to re-render their children nor needing any performance hints.

References

Thanks 👋

State Mechanisms In JavaScript Frameworks

By Abdelrahman Awad

State Mechanisms In JavaScript Frameworks

  • 220
Loading comments...

More from Abdelrahman Awad