Перейти к содержимому
GitHub

$effect

Эффекты — это функции, которые выполняются при обновлении состояния и могут использоваться для таких задач, как вызов сторонних библиотек, рисование на элементах <canvas> или выполнение сетевых запросов. Они выполняются только в браузере, а не во время серверного рендеринга.

Вообще говоря, вам не следует обновлять состояние внутри эффектов, так как это может сделать код более запутанным и часто приводит к бесконечным циклам обновлений. Если вы обнаруживаете, что делаете это, ознакомьтесь с разделом Когда не использовать $effect, чтобы узнать об альтернативных подходах.

Вы можете создать эффект с помощью руны $effect (демонстрация):

<script>
  let size = $state(50);
  let color = $state('#ff3e00');

  let canvas;

  $effect(() => {
    const context = canvas.getContext('2d');
    context.clearRect(0, 0, canvas.width, canvas.height);

    // это будет повторно выполняться всякий раз, когда изменяются `color` или `size`
    context.fillStyle = color;
    context.fillRect(0, 0, size, size);
  });
</script>

<canvas bind:this={canvas} width="100" height="100" />

Когда Svelte выполняет функцию эффекта, она отслеживает, какие части состояния (и производного состояния) были доступны (если они не были доступны внутри untrack), и повторно запускает функцию, когда это состояние изменяется.

Понимание жизненного цикла

Ваши эффекты выполняются после того, как компонент был смонтирован в DOM, и в рамках микрозадачи после изменений состояния. Повторные запуски группируются (например, изменение color и size одновременно не вызовет двух отдельных запусков) и происходят после применения всех обновлений DOM.

Вы можете использовать $effect где угодно, не только на верхнем уровне компонента, при условии, что он вызывается во время выполнения родительского эффекта.

Эффект может возвращать функцию очистки, которая будет выполняться непосредственно перед повторным запуском эффекта (демонстрация).

<script>
  let count = $state(0);
  let milliseconds = $state(1000);

  $effect(() => {
    // Это будет пересоздано всякий раз, когда изменяется `milliseconds`
    const interval = setInterval(() => {
      count += 1;
    }, milliseconds);

    return () => {
      // если предоставлена функция очистки, она будет выполнена
      // a) немедленно перед повторным выполнением эффекта
      // b) когда компонент будет уничтожен
      clearInterval(interval);
    };
  });
</script>

<h1>{count}</h1>

<button onclick={() => (milliseconds *= 2)}>медленнее</button>
<button onclick={() => (milliseconds /= 2)}>быстрее</button>

Функции очистки также выполняются, когда эффект уничтожается, что происходит при уничтожении его родителя (например, когда компонент демонтируется) или при повторном запуске родительского эффекта.

Понимание зависимостей

$effect автоматически отслеживает любые реактивные значения ($state, $derived, $props), которые синхронно читаются внутри его тела функции (включая косвенные вызовы через другие функции), и регистрирует их как зависимости. Когда эти зависимости изменяются, $effect планирует повторный запуск.

Если $state и $derived используются непосредственно внутри $effect (например, при создании реактивного класса), эти значения не будут рассматриваться как зависимости.

Значения, которые читаются асинхронно — после await или внутри setTimeout, например — не будут отслеживаться. Здесь холст будет перерисован, когда изменится color, но не когда изменится size (демонстрация):

$effect(() => {
  const context = canvas.getContext('2d');
  context.clearRect(0, 0, canvas.width, canvas.height);

  // это будет повторно выполняться всякий раз, когда изменяется `color`...
  context.fillStyle = color;

  setTimeout(() => {
    // ...но не когда изменяется `size`
    context.fillRect(0, 0, size, size);
  }, 0);
});

Эффект повторно выполняется только тогда, когда изменяется сам объект, который он читает, а не когда изменяется свойство внутри него. (Если вы хотите отслеживать изменения внутри объекта во время разработки, вы можете использовать $inspect.)

<script>
  let state = $state({ value: 0 });
  let derived = $derived({ value: state.value * 2 });

  // это выполнится один раз, потому что `state` никогда не переназначается (только мутируется)
  $effect(() => {
    state;
  });

  // это будет выполняться всякий раз, когда изменяется `state.value`...
  $effect(() => {
    state.value;
  });

  // ...и это также будет выполняться, потому что `derived` — это новый объект каждый раз
  $effect(() => {
    derived;
  });
</script>

<button onclick={() => (state.value += 1)}>
  {state.value}
</button>

<p>{state.value} вдвое больше — это {derived.value}</p>

Эффект зависит только от значений, которые он считал в последний раз, когда выполнялся. Это имеет интересные последствия для эффектов с условным кодом.

Например, если a истинно в приведённом ниже фрагменте кода, код внутри блока if будет выполнен, и b будет оценено. Таким образом, изменения как a, так и b приведут к повторному выполнению эффекта.

С другой стороны, если a ложно, b не будет оценено, и эффект будет повторно выполняться только при изменении a.

$effect(() => {
  console.log('running');

  if (a) {
    console.log('b:', b);
  }
});

$effect.pre

В редких случаях вам может понадобиться выполнить код до обновления DOM. Для этого мы можем использовать руну $effect.pre:

<script>
  import { tick } from 'svelte';

  let div = $state();
  let messages = $state([]);

  // ...

  $effect.pre(() => {
    if (!div) return; // ещё не смонтирован

    // ссылайтесь на длину массива `messages`, чтобы этот код повторно выполнялся всякий раз, когда он изменяется
    messages.length;

    // автопрокрутка при добавлении новых сообщений
    if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
      tick().then(() => {
        div.scrollTo(0, div.scrollHeight);
      });
    }
  });
</script>

<div bind:this={div}>
  {#each messages as message}
    <p>{message}</p>
  {/each}
</div>

Помимо времени выполнения, $effect.pre работает точно так же, как и $effect.

$effect.tracking

Руна $effect.tracking — это продвинутая функция, которая сообщает вам, выполняется ли код внутри контекста отслеживания, такого как эффект или внутри вашего шаблона (демонстрация):

<script>
  console.log('in component setup:', $effect.tracking()); // false

  $effect(() => {
    console.log('in effect:', $effect.tracking()); // true
  });
</script>

<p>в шаблоне: {$effect.tracking()}</p> <!-- true -->

Это используется для реализации абстракций, таких как createSubscriber, которая создает слушателей для обновления реактивных значений, но только если эти значения отслеживаются (в отличие, например, от значений, читаемых внутри обработчика событий).

$effect.root

Руна $effect.root — это продвинутая функция, создающая неконтролируемую область, которая не очищается автоматически. Это полезно для вложенных эффектов, которые вы хотите контролировать вручную. Эта руна также позволяет создавать эффекты вне фазы инициализации компонента.

<script>
  let count = $state(0);

  const cleanup = $effect.root(() => {
    $effect(() => {
      console.log(count);
    });

    return () => {
      console.log('очистка корневого эффекта');
    };
  });
</script>

Когда не использовать $effect

В общем, $effect лучше рассматривать как некий выходной механизм — полезный для таких вещей, как аналитика и прямое манипулирование DOM — а не как инструмент, который следует использовать часто. В частности, избегайте использования его для синхронизации состояния. Вместо этого…

<script>
  let count = $state(0);
  let doubled = $state();

  // не делайте так!
  $effect(() => {
    doubled = count * 2;
  });
</script>

…делайте так:

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

Если вы используете эффект, потому что хотите иметь возможность переопределять производное значение (например, для создания оптимистичного UI), обратите внимание, что производные значения можно напрямую переопределять, начиная со Svelte 5.25.

Вас может привлечь идея создать сложные связи между эффектами, чтобы связать одно значение с другим. В следующем примере представлены два поля ввода для «потраченных денег» и «оставшихся денег», которые взаимосвязаны. Если вы измените одно из них, другое должно обновиться соответственно. Не используйте эффекты для этого (демонстрация):

<script>
  let total = 100;
  let spent = $state(0);
  let left = $state(total);

  $effect(() => {
    left = total - spent;
  });

  $effect(() => {
    spent = total - left;
  });
</script>

<label>
  <input type="range" bind:value={spent} max={total} />
  {spent}/{total} потрачено
</label>

<label>
  <input type="range" bind:value={left} max={total} />
  {left}/{total} осталось
</label>

Вместо этого используйте обратные вызовы, где это возможно (демонстрация):

<script>
  let total = 100;
  let spent = $state(0);
  let left = $state(total);

  function updateSpent(e) {
    spent = +e.target.value;
    left = total - spent;
  }

  function updateLeft(e) {
    left = +e.target.value;
    spent = total - left;
  }
</script>

<label>
  <input type="range" value={spent} oninput={updateSpent} max={total} />
  {spent}/{total} потрачено
</label>

<label>
  <input type="range" value={left} oninput={updateLeft} max={total} />
  {left}/{total} осталось
</label>

Если вам по какой-либо причине нужно использовать привязки (например, когда вы хотите нечто вроде «записываемого $derived»), рассмотрите возможность использования геттеров и сеттеров для синхронизации состояния (демонстрация):

<script>
  let total = 100;
  let spent = $state(0);

  let left = {
    get value() {
      return total - spent;
    },
    set value(v) {
      spent = total - v;
    }
  };
</script>

<label>
  <input type="range" bind:value={spent} max={total} />
  {spent}/{total} потрачено
</label>

<label>
  <input type="range" bind:value={left.value} max={total} />
  {left.value}/{total} осталось
</label>

Если вам абсолютно необходимо обновить $state внутри эффекта и вы столкнулись с бесконечным циклом, потому что читаете и записываете в одно и то же состояние $state, используйте untrack.