UNPKG

svelte

Version:

Cybernetically enhanced web apps

82 lines (75 loc) 2.57 kB
import { get, tick, untrack } from '../internal/client/runtime.js'; import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; import { source } from '../internal/client/reactivity/sources.js'; import { increment } from './utils.js'; /** * Returns a `subscribe` function that, if called in an effect (including expressions in the template), * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. * * If `start` returns a function, it will be called when the effect is destroyed. * * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects * are active, and the returned teardown function will only be called when all effects are destroyed. * * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery): * * ```js * import { createSubscriber } from 'svelte/reactivity'; * import { on } from 'svelte/events'; * * export class MediaQuery { * #query; * #subscribe; * * constructor(query) { * this.#query = window.matchMedia(`(${query})`); * * this.#subscribe = createSubscriber((update) => { * // when the `change` event occurs, re-run any effects that read `this.current` * const off = on(this.#query, 'change', update); * * // stop listening when all the effects are destroyed * return () => off(); * }); * } * * get current() { * this.#subscribe(); * * // Return the current state of the query, whether or not we're in an effect * return this.#query.matches; * } * } * ``` * @param {(update: () => void) => (() => void) | void} start * @since 5.7.0 */ export function createSubscriber(start) { let subscribers = 0; let version = source(0); /** @type {(() => void) | void} */ let stop; return () => { if (effect_tracking()) { get(version); render_effect(() => { if (subscribers === 0) { stop = untrack(() => start(() => increment(version))); } subscribers += 1; return () => { tick().then(() => { // Only count down after timeout, else we would reach 0 before our own render effect reruns, // but reach 1 again when the tick callback of the prior teardown runs. That would mean we // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. subscribers -= 1; if (subscribers === 0) { stop?.(); stop = undefined; } }); }; }); } }; }