UNPKG

classy-solid

Version:

Solid.js reactivity patterns for classes, and class components.

111 lines (83 loc) 2.97 kB
// TODO switch to non-dep-tracking non-queue-modifying deferred signals, because those do not break with regular effects. import {createSignal as _createSignal, createEffect, onCleanup, getOwner, runWithOwner} from 'solid-js' import type {EffectFunction} from 'solid-js/types/reactive/signal' const effectQueue: Set<EffectFunction<any>> = new Set() let runningEffects = false // map of effects to dependencies const effectDeps = new Map<EffectFunction<any>, Set<(v: any) => any>>() let currentEffect: EffectFunction<any> = () => {} // Override createSignal in order to implement custom tracking of effect // dependencies, so that when signals change, we are aware which dependenct // effects need to be moved to the end of the effect queue while running // deferred effects in a microtask. export let createSignal = ((value, options) => { let [_get, _set] = _createSignal(value, options) const get = (() => { if (!runningEffects) return _get() let deps = effectDeps.get(currentEffect) if (!deps) effectDeps.set(currentEffect, (deps = new Set())) deps.add(_set) return _get() }) as typeof _get const set = (v => { if (!runningEffects) return _set(v as any) // This is inefficient, for proof of concept, unable to use Solid // internals on the outside. for (const [fn, deps] of effectDeps) { for (const dep of deps) { if (dep === _set) { // move to the end effectQueue.delete(fn) effectQueue.add(fn) } } } return _set(v as any) }) as typeof _set return [get, set] }) as typeof _createSignal let effectTaskIsScheduled = false // TODO Option so the first run is deferred instead of immediate? This already // happens outside of a root. export const createDeferredEffect = ((fn, value, options) => { let initial = true createEffect( (prev: any) => { if (initial) { initial = false currentEffect = fn effectDeps.get(fn)?.clear() // clear to track deps, or else it won't track new deps based on code branching fn(prev) return } effectQueue.add(fn) // add, or move to the end, of the queue. TODO This is probably redundant now, but I haven't tested yet. // If we're currently running the queue, return because fn will run // again at the end of the queue iteration due to our overriden // createSignal moving it to the end. if (runningEffects) return if (effectTaskIsScheduled) return effectTaskIsScheduled = true const owner = getOwner() queueMicrotask(() => { if (owner) runWithOwner(owner, runEffects) else runEffects() }) }, value, options, ) getOwner() && onCleanup(() => { effectDeps.delete(fn) effectQueue.delete(fn) }) }) as typeof createEffect function runEffects() { runningEffects = true for (const fn of effectQueue) { effectQueue.delete(fn) // TODO This is probably redundant now, but I haven't tested yet. createDeferredEffect(fn) } runningEffects = false effectTaskIsScheduled = false }