Masterclass
Signals

What is Reactivity?

let a = 3
let a = 3
let b = a * 10
let a = 3
let b = a * 10
console.log(b) // 30
let a = 3
let b = a * 10
console.log(b) // 30

a = 4
console.log(b) // 30
let a = 3
let b = a * 10
console.log(b) // 30

a = 4
console.log(b) // 30

b = a * 10
console.log(b) // 40

 A  B
 1  4  40

(fx = A1 * 10)

// change tracker
whenAChanges(() => {
  b = a * 10
})
 A  B C
 1  4  40
<template>
  <span id="cell-b1"></span>
</template>

<script>
</script>
 A  B C
 1  4  40
<template>
  <span id="cell-b1"></span>
</template>

<script>
document
  .getElementById('cell-b1')
  .textContent = state.a * 10
</script>
 A  B C
 1  4  40
<template>
  <span id="cell-b1"></span>
</template>

<script>
whenStateChanges(() => {
  document
    .getElementById('cell-b1')
    .textContent = state.a * 10
});
</script>
 A  B C
 1  4  40
<template>
  <span id="cell-b1" />
  <button id="inc">Inc</button>
</template>

<script>
whenStateChanges(() => {
  document
    .getElementById('cell-b1')
    .textContent = state.a * 10 
});
</script>

<script>
const btn = document.getElementById('inc')
btn.addEventListener('click', () => {
  state.a = 4;
});
</script>

Track & Trigger

Track & Trigger

Value set/get Interception

const [value, setValue] = createSignal(0)
setValue(value() + 1)

Value set/get interception (signal)

track (dependencies)  & trigger

+

whenStateChanges (effect)

+

=

reactivity system

Flavors

{
  data() {
    let count = 1
    return {
      get count() {
        track()
        return count
      },
      set count(newCount) {
        count = newCount
        trigger()  
      }
    }
  }
}

Vue2

{
  data() {
    return {
      count: 1
    }
  }
}

Property Interception

>>

function ref(initialValue) {
  let value = initialValue
  return {
    get value() {
      track()
      return value
    },
    set value(newValue) {
      value = newValue
      trigger()
    }
  }
}

Vue3

const count = ref(0)
count.value = count.value + 1

refs

const [count, setCount] = createSignal(0)
setCount(count() + 1)

Solid

function createSignal(initialValue) {
  let _value = initialValue
  function get() {
    track()
    return _value
  };
  function set(val) {
    _value = val
    trigger();
  }
  return [get, set]
}

signals

const count = signal(0)
count.value = 1

Preact

function signal(initialValue) {
  let value = initialValue
  return {
    get value() {
      track()
      return value
    },
    set value(newValue) {
      value = newValue
      trigger()
    }
  }
}

signals

const count = signal(0)
count.set(count() + 1)

Angular v16

function signal(initialValue) {
  let value = initialValue
  function getter() {
    track()
    return value
  }
  getter.set = function(newValue) {
    value = newValue
    trigger()
  }
  return getter
}

signals

Implementing
a Reactive System

let activeEffect

class Dep {
  subscribers = new Set()
  track() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }

  trigger() {
    this.subscribers.forEach((sub) => sub())
  }
}

function createEffect(cb) {
  activeEffect = cb
  cb()
  activeEffect = null
}

Dependency tracker

function createSignal(value) {
  // use Dep to implement a signal
  const dep = new Dep()
  function get() {
    dep.track()
    return value
  }
  function set(newValue) {
    value = newValue
    dep.trigger()
  }
  return [get, set]
}

// usage
const [count, setCount] = createSignal(0)

createSignal

Like Solid createSignal

const [count, setCount] = createSignal(0)

// attach button handler
const btn = document.getElementById('inc')
btn.addEventListener('click', () => {
  setCount(count() + 1)
});

// response to changes
createEffect(() => {
  const elem = document.getElementById('count')
  elem.textContent = 'count is' + count();
});

Using signal

<body>
  <div id="count">0</div>
  <button id="inc">Increment</button>
</body>

Implementing
Vue3

function ref(value) {
  const dep = new Dep()
  return {
    get value() {
      dep.track()
      return value
    },
    set value(newValue) {
      value = newValue
      dep.trigger()
    }
  }
}

// usage
const count = ref(0)

Vue3 ref

const appComponent = {
  template: `
    <h2 class="text-xl font-bold">Mini Vue</h2>
    <div :class="count.value % 2 ? 'text-red-400' : 'text-green-600'">
      count is: {{ count.value }}
    </div>
    <button @click="increment()" class="btn btn-primary">
      Inc
    </button>
  `,
  setup() {
    const count = ref(0);
    return {
      count,
      increment() {
        count.value++;
      },
    };
  },
};

const app = createApp(appComponent);
app.mount("#app");

Vue3 app

function createApp(rootComponent) {
  return {
    mount(target) {
      // get state
      const state = rootComponent.setup();

      // take the template string and set innerHTML
      const container = document.querySelector(target);
      container.innerHTML = rootComponent.template;

      // walk the resulting DOM tree and look 
      // for directives or bindings like 
      // `@click` and `{{ }}`
      render(container, state);
    },
  };
}

Vue3 app

function render(root, state) {
  for (const node of root.childNodes) {
    if (node.nodeType === 1 /* ELEMENT */) {
      for (const { name, value } of node.attributes) {
        if (name === "@click") {
          node.addEventListener("click", () => {
            evaluateExp(value, state);
          });
        } else if (name === ":class") {
          effect(() => {
            node.className = evaluateExp(value, state);
          });
        }
      }
    }
    else if (node.nodeType === 3 /* TEXT */) {
      // handle text
    }
  }
}

Vue3 app

The complete code and reference project can be found at

github

Credits

Compacted and simplified  by Peter Cosemans

 

For Euricom DevCruise 2023

MasterclassSignals

By Peter Cosemans

MasterclassSignals

  • 68