Как Angular Signals работает внутри?

Angular signal — это новая красивая функция, которая поможет разработчикам делать приложения проще, быстрее и писать меньше кода. Но когда видишь это впервые, немного сложно понять, как это работает. Когда вы читаете статьи об «Angular Signals», вы можете найти много информации о том, как его использовать, но нет информации о том, как он работает. Я думаю, что самый простой способ освоить утилиту — это понять, как она работает изнутри. Итак, я хочу показать, как работает угловой сигнал изнутри, создавая простую версию сигналов.

Шаг 1. Сигнальная функция

Как мы знаем, сигнал — это просто функция, которая возвращает функцию, которая возвращает значение. Итак, давайте создадим его:

function signal(value) {
  function getter() { return value; }
  return getter;
}

const a = signal(10)
console.log(a()) // console output: 10

Шаг 2. Обновление значений

Когда сигнал создан, нам нужен способ изменить их значение. Давайте расширим `getter` с помощью метода `set`:

function signal(value) {
  function getter() { return value; }
  getter.set = (newValue) => value = newValue;
  return getter;
}

const a = signal(10)
console.log(a()) // console output: 10
a.set(11)
console.log(a()) // console output: 11

Теперь у нас есть изменяемое значение.

Шаг 3. Функция эффекта

На первый взгляд

function signal(value) {
  function getter() { return value; }
  getter.set = (newValue) => value = newValue;
  return getter;
}

function effect(callback) {
  callback();
}

const a = signal(10);

effect(() => console.log(a())) // Console output: 10

Это работает, но как подключить эффект и сигнал меняется. Как мы можем уведомить сигнал о том, что он соответствует эффекту? Решение состоит в том, чтобы сделать флаг, который мы изменим в начале и в конце функции эффекта.

let inEffect = false;

function signal(value) {
  function getter() {
    console.log(`getted from efffect: ${inEffect}`)
    return value;
  }
  getter.set = (newValue) => value = newValue;
  return getter;
}

function effect(callback) {
  inEffect = true;
  callback();
  inEffect = false;
}

const a = signal(10);

a(); // console output: getted from efffect: false
effect(() => a()); // console output: getted from efffect: true

Шаг 4. Магия

Теперь мы заменим флаг функцией обратного вызова, и каждый раз, когда кто-то вызывает `getter`, мы будем проверять обратный вызов и сохранять его для каждого сигнала. И когда кто-то вызовет `set`, мы вызовем все сохраненные обратные вызовы;

let currentCallback;

function signal(value) {
  const callbacks = [];
  function getter() {
    if (currentCallback) { callbacks.push(currentCallback) }
    return value;
  }

  getter.set = (newValue) => {
    value = newValue;
    callbacks.forEach(cb => cb());
  }
  
  return getter;
}

function effect(callback) {
  currentCallback = callback;
  callback();
  currentCallback = undefined;
}

Давайте проверим это:

const a = signal(0);
const b = signal('a');

effect(() => console.log(`This effect depends from a: ${a()}`));
// console: This effect depends from a: 0
effect(() => console.log(`This effect depends from b: ${b()}`));
// console: This effect depends from b: a
effect(() => console.log(`This effect depends from a: ${a()} and b: ${b()}`));
// console: This effect depends from a: 0 and b: a

a.set(1);
// console: This effect depends from a: 1
// console: This effect depends from a: 1 and b: a 
b.set('b');
// console: This effect depends from b: b
// console: This effect depends from a: 1 and b: b

Шаг 5. Расчет

Это просто комбинация сигнала и эффекта

function computed(fn) {
  const s = signal(null);
  effect(() => s.set(fn()))
  return s;
}

и теперь мы можем сделать это

const num = signal(0);
const isEven = computed(() => num() % 2 ? 'odd' : 'event')

effect(() => console.log(`Number ${num()} is ${isEven()}`));

Заключение

Я надеюсь, что это поможет вам понять сигналы, и вам будет легче интегрировать их в свои проекты.

Полный код из этой статьи: github
Код оригинальных сигналов Angular: github