Skip to content

Instantly share code, notes, and snippets.

@jung-han
Last active February 11, 2024 07:31
Show Gist options
  • Save jung-han/62bbbf669926853be94086cc1f34c25e to your computer and use it in GitHub Desktop.
Save jung-han/62bbbf669926853be94086cc1f34c25e to your computer and use it in GitHub Desktop.
packages/core/src/index.ts
// core v1.5.1 기준
function cycleDetected(): never {
throw new Error("Cycle detected");
}
function mutationDetected(): never {
throw new Error("Computed cannot have side-effects");
}
const identifier = Symbol.for("preact-signals");
// Computed와 Effect에 사용되는 플래그
const RUNNING = 1 << 0;
const NOTIFIED = 1 << 1;
const OUTDATED = 1 << 2;
const DISPOSED = 1 << 3;
const HAS_ERROR = 1 << 4;
const TRACKING = 1 << 5;
// 종속성(소스)과 종속 요소(대상)를 추적하는 데 사용되는 링크된 목록 노드입니다.
// 타깃이 마지막으로 본 소스의 버전 번호를 기억하는 데에도 사용됩니다.
type Node = {
// 타겟이 의존성을 갖는 값의 소스입니다.
_source: Signal;
_prevSource?: Node;
_nextSource?: Node;
// 소스에 의존하는 대상이며, 소스가 변경되면 알려야 하는 대상입니다.
_target: Computed | Effect;
_prevTarget?: Node;
_nextTarget?: Node;
// 타겟이 마지막으로 본 소스의 버전 번호입니다. 우리는 소스 값을 저장하는 대신 버전 번호를 사용합니다.
// 왜냐하면 소스 값은 임의의 양의 메모리를 차지할 수 있고 계산된 값은 느리게 평가되기 때문에 영원히 유지될 수 있기 때문입니다.
// 잠재적으로 사용되지 않지만 재활용 가능한 노드를 표시하려면 특수 값 -1을 사용하십시오.
_version: number;
// 새로운 평가 컨텍스트에 들어가고 나갈 때 소스의 이전 `._node` 값을 기억하고 롤백하는 데 사용됩니다.
_rollbackNode?: Node;
};
function startBatch() {
batchDepth++;
}
function endBatch() {
if (batchDepth > 1) {
batchDepth--;
return;
}
let error: unknown;
let hasError = false;
while (batchedEffect !== undefined) {
let effect: Effect | undefined = batchedEffect;
batchedEffect = undefined;
batchIteration++;
while (effect !== undefined) {
const next: Effect | undefined = effect._nextBatchedEffect;
effect._nextBatchedEffect = undefined;
effect._flags &= ~NOTIFIED;
if (!(effect._flags & DISPOSED) && needsToRecompute(effect)) {
try {
effect._callback();
} catch (err) {
if (!hasError) {
error = err;
hasError = true;
}
}
}
effect = next;
}
}
batchIteration = 0;
batchDepth--;
if (hasError) {
throw error;
}
}
function batch<T>(callback: () => T): T {
if (batchDepth > 0) {
return callback();
}
/*@__INLINE__**/ startBatch();
try {
return callback();
} finally {
endBatch();
}
}
// 현재 수행된(evaluated) computed 또는 effect입니다.
let evalContext: Computed | Effect | undefined = undefined;
let untrackedDepth = 0;
function untracked<T>(callback: () => T): T {
if (untrackedDepth > 0) {
return callback();
}
const prevContext = evalContext;
evalContext = undefined;
untrackedDepth++;
try {
return callback();
} finally {
untrackedDepth--;
evalContext = prevContext;
}
}
// 배치로 수집된 effect
let batchedEffect: Effect | undefined = undefined;
let batchDepth = 0;
let batchIteration = 0;
// 전역적으로 변경된 사항이 없을 때 반복되는 계산.
// peek()/computed.value 호출을 빠르게 경로 지정하는 데 사용되는 signal의 전역 버전 번호입니다.
let globalVersion = 0;
function addDependency(signal: Signal): Node | undefined {
if (evalContext === undefined) {
return undefined;
}
let node = signal._node;
if (node === undefined || node._target !== evalContext) {
/**
* `signal`은 새로운 종속성입니다. 새 종속성 노드를 생성하고
* 이를 현재 컨텍스트 종속성 목록의 꼬리로 설정합니다. 예:
* { A <-> B }
* ↑ ↑
* tail node (new)
* ↓
* { A <-> B <-> C }
* ↑
* tail (evalContext._sources)
*/
node = {
_version: 0,
_source: signal,
_prevSource: evalContext._sources,
_nextSource: undefined,
_target: evalContext,
_prevTarget: undefined,
_nextTarget: undefined,
_rollbackNode: node,
};
if (evalContext._sources !== undefined) {
evalContext._sources._nextSource = node;
}
evalContext._sources = node;
signal._node = node;
// effect를 수행하거나 구독자가 있는 computed signal를 수행하는 경우
// 이 종속성에서 변경 알림을 구독하세요.
if (evalContext._flags & TRACKING) {
signal._subscribe(node);
}
return node;
} else if (node._version === -1) {
// `signal`은 이전 수행의 기존 종속성입니다. 재사용하세요.
node._version = 0;
/**
* '노드'가 아직 종속성 목록의 현재 마지막(꼬리)이 아닌 경우(즉, 목록에 다음 노드가 있는 경우)
* '노드'를 새 마지막 노드로 만듭니다. eg
*
* { A <-> B <-> C <-> D }
* ↑ ↑
* node ┌─── tail (evalContext._sources)
* └─────│─────┐
* ↓ ↓
* { A <-> C <-> D <-> B }
* ↑
* tail (evalContext._sources)
*/
if (node._nextSource !== undefined) {
node._nextSource._prevSource = node._prevSource;
if (node._prevSource !== undefined) {
node._prevSource._nextSource = node._nextSource;
}
node._prevSource = evalContext._sources;
node._nextSource = undefined;
evalContext._sources!._nextSource = node;
evalContext._sources = node;
}
// 현재 계산된 effect/computed signal은 필요한 경우
// 'signal'의 변경 알림을 이미 구독했다고 가정할 수 있습니다.
return node;
}
return undefined;
}
// @ts-ignore 내부 Signal은 함수로 간주합니다.
declare class Signal<T = any> {
/** @internal */
_value: unknown;
/**
* @internal
* 버전 번호는 항상 0보다 커야 합니다.
* 특수 값 -1은 잠재적으로 사용되지 않지만 재활용 가능한 노드를 나타내기 위해 노드에서 사용되기 때문입니다.
*/
_version: number;
/** @internal */
_node?: Node;
/** @internal */
_targets?: Node;
constructor(value?: T);
/** @internal */
_refresh(): boolean;
/** @internal */
_subscribe(node: Node): void;
/** @internal */
_unsubscribe(node: Node): void;
subscribe(fn: (value: T) => void): () => void;
valueOf(): T;
toString(): string;
toJSON(): T;
peek(): T;
brand: typeof identifier;
get value(): T;
set value(value: T);
}
/** @internal */
// @ts-ignore 내부 Signal은 함수로 간주합니다.
function Signal(this: Signal, value?: unknown) {
this._value = value;
this._version = 0;
this._node = undefined;
this._targets = undefined;
}
Signal.prototype.brand = identifier;
Signal.prototype._refresh = function () {
return true;
};
Signal.prototype._subscribe = function (node) {
if (this._targets !== node && node._prevTarget === undefined) {
node._nextTarget = this._targets;
if (this._targets !== undefined) {
this._targets._prevTarget = node;
}
this._targets = node;
}
};
Signal.prototype._unsubscribe = function (node) {
// signal에 구독자가 있는 경우에만 구독 취소 단계를 실행하세요.
if (this._targets !== undefined) {
const prev = node._prevTarget;
const next = node._nextTarget;
if (prev !== undefined) {
prev._nextTarget = next;
node._prevTarget = undefined;
}
if (next !== undefined) {
next._prevTarget = prev;
node._nextTarget = undefined;
}
if (node === this._targets) {
this._targets = next;
}
}
};
Signal.prototype.subscribe = function (fn) {
const signal = this;
return effect(function (this: Effect) {
const value = signal.value;
const flag = this._flags & TRACKING;
this._flags &= ~TRACKING;
try {
fn(value);
} finally {
this._flags |= flag;
}
});
};
Signal.prototype.valueOf = function () {
return this.value;
};
Signal.prototype.toString = function () {
return this.value + "";
};
Signal.prototype.toJSON = function () {
return this.value;
};
Signal.prototype.peek = function () {
return this._value;
};
Object.defineProperty(Signal.prototype, "value", {
get() {
const node = addDependency(this);
if (node !== undefined) {
node._version = this._version;
}
return this._value;
},
set(this: Signal, value) {
if (evalContext instanceof Computed) {
mutationDetected();
}
if (value !== this._value) {
if (batchIteration > 100) {
cycleDetected();
}
this._value = value;
this._version++;
globalVersion++;
/**@__INLINE__*/ startBatch();
try {
for (
let node = this._targets;
node !== undefined;
node = node._nextTarget
) {
node._target._notify();
}
} finally {
endBatch();
}
}
},
});
function signal<T>(value: T): Signal<T> {
return new Signal(value);
}
function needsToRecompute(target: Computed | Effect): boolean {
// 변경된 값에 대한 종속성을 확인하십시오. 종속성 목록은 이미 사용 순서대로 되어 있습니다.
// 따라서 여러 종속성에서 값이 변경된 경우 이 시점에서는 처음 사용된 종속성만 다시 평가됩니다.
for (
let node = target._sources;
node !== undefined;
node = node._nextSource
) {
// 새로 고침 전이나 후에 종속성의 새 버전이 있거나
// 종속성에 전혀 새로 고침을 차단하는 요소가 있는 경우(예: 종속성 주기) 다시 계산해야 합니다.
if (
node._source._version !== node._version ||
!node._source._refresh() ||
node._source._version !== node._version
) {
return true;
}
}
// 마지막 재계산 이후 종속 항목 중 값이 변경되지 않은 경우
// 다시 계산할 필요가 없습니다.
return false;
}
function prepareSources(target: Computed | Effect) {
/**
* 1. 현재 소스를 모두 재사용 가능한 노드로 표시합니다(버전: -1).
* 2. 현재 노드가 다른 컨텍스트에서 사용되고 있는 경우 롤백 노드를 설정합니다.
* 3. 이중 연결 목록의 꼬리에 'target._sources'를 지정합니다. 예:
*
* { undefined <- A <-> B <-> C -> undefined }
* ↑ ↑
* │ └──────┐
* target._sources = A; (node is head) │
* ↓ │
* target._sources = C; (node is tail) ─┘
*/
for (
let node = target._sources;
node !== undefined;
node = node._nextSource
) {
const rollbackNode = node._source._node;
if (rollbackNode !== undefined) {
node._rollbackNode = rollbackNode;
}
node._source._node = node;
node._version = -1;
if (node._nextSource === undefined) {
target._sources = node;
break;
}
}
}
function cleanupSources(target: Computed | Effect) {
let node = target._sources;
let head = undefined;
/**
* 이 시점에 'target._sources'는 이중 링크드 리스트의 꼬리를 가리킵니다.
* 기존 소스 + 신규 소스를 모두 사용 순서대로 포함하고 있습니다.
* 이전 종속성을 삭제하면서 헤드 노드를 찾을 때까지 뒤로 반복합니다.
*/
while (node !== undefined) {
const prev = node._prevSource;
/**
* 노드가 재사용되지 않았습니다. 변경 알림 구독을 취소하고 이중 링크드 리스트에서 노드 자체를 제거합니다. 예:
*
* { A <-> B <-> C }
* ↓
* { A <-> C }
*/
if (node._version === -1) {
node._source._unsubscribe(node);
if (prev !== undefined) {
prev._nextSource = node._nextSource;
}
if (node._nextSource !== undefined) {
node._nextSource._prevSource = prev;
}
} else {
/**
* 새 헤드는 이중 링크드 리스트 에서 제거/구독 취소되지 않은 마지막 노드입니다. 예:
*
* { A <-> B <-> C }
* ↑ ↑ ↑
* │ │ └ head = node
* │ └ head = node
* └ head = node
*/
head = node;
}
node._source._node = node._rollbackNode;
if (node._rollbackNode !== undefined) {
node._rollbackNode = undefined;
}
node = prev;
}
target._sources = head;
}
declare class Computed<T = any> extends Signal<T> {
_compute: () => T;
_sources?: Node;
_globalVersion: number;
_flags: number;
constructor(compute: () => T);
_notify(): void;
get value(): T;
}
function Computed(this: Computed, compute: () => unknown) {
Signal.call(this, undefined);
this._compute = compute;
this._sources = undefined;
this._globalVersion = globalVersion - 1;
this._flags = OUTDATED;
}
Computed.prototype = new Signal() as Computed;
Computed.prototype._refresh = function () {
this._flags &= ~NOTIFIED;
if (this._flags & RUNNING) {
return false;
}
// 이 computed signal가 종속성(TRACKING 플래그 설정)의 업데이트를 구독하고
// 그 중 어느 것도 변경 사항을 알리지 않은 경우(OUTDATED 플래그가 설정되지 않음) 계산된 값은 변경될 수 없습니다.
if ((this._flags & (OUTDATED | TRACKING)) === TRACKING) {
return true;
}
this._flags &= ~OUTDATED;
if (this._globalVersion === globalVersion) {
return true;
}
this._globalVersion = globalVersion;
// 값 변경에 대한 종속성을 확인하기 전에 이 computed signal이 실행 중임을 표시하여
// RUNNING 플래그를 사용하여 순환 종속성을 확인할 수 있도록 합니다.
this._flags |= RUNNING;
if (this._version > 0 && !needsToRecompute(this)) {
this._flags &= ~RUNNING;
return true;
}
const prevContext = evalContext;
try {
prepareSources(this);
evalContext = this;
const value = this._compute();
if (
this._flags & HAS_ERROR ||
this._value !== value ||
this._version === 0
) {
this._value = value;
this._flags &= ~HAS_ERROR;
this._version++;
}
} catch (err) {
this._value = err;
this._flags |= HAS_ERROR;
this._version++;
}
evalContext = prevContext;
cleanupSources(this);
this._flags &= ~RUNNING;
return true;
};
Computed.prototype._subscribe = function (node) {
if (this._targets === undefined) {
this._flags |= OUTDATED | TRACKING;
// computed signal은 첫 번째 구독자를 얻을 때 종속성을 느리게 구독합니다.
for (
let node = this._sources;
node !== undefined;
node = node._nextSource
) {
node._source._subscribe(node);
}
}
Signal.prototype._subscribe.call(this, node);
};
Computed.prototype._unsubscribe = function (node) {
// computed signal에 구독자가 있는 경우에만 구독 취소 단계를 실행하세요.
if (this._targets !== undefined) {
Signal.prototype._unsubscribe.call(this, node);
// computed signal은 마지막 구독자를 잃으면 종속성 구독을 취소합니다.
// 이를 통해 computed signal의 참조 해제된 하위 그래프를 가비지 수집할 수 있습니다.
if (this._targets === undefined) {
this._flags &= ~TRACKING;
for (
let node = this._sources;
node !== undefined;
node = node._nextSource
) {
node._source._unsubscribe(node);
}
}
}
};
Computed.prototype._notify = function () {
if (!(this._flags & NOTIFIED)) {
this._flags |= OUTDATED | NOTIFIED;
for (
let node = this._targets;
node !== undefined;
node = node._nextTarget
) {
node._target._notify();
}
}
};
Computed.prototype.peek = function () {
if (!this._refresh()) {
cycleDetected();
}
if (this._flags & HAS_ERROR) {
throw this._value;
}
return this._value;
};
Object.defineProperty(Computed.prototype, "value", {
get() {
if (this._flags & RUNNING) {
cycleDetected();
}
const node = addDependency(this);
this._refresh();
if (node !== undefined) {
node._version = this._version;
}
if (this._flags & HAS_ERROR) {
throw this._value;
}
return this._value;
},
});
interface ReadonlySignal<T = any> extends Signal<T> {
readonly value: T;
}
function computed<T>(compute: () => T): ReadonlySignal<T> {
return new Computed(compute);
}
function cleanupEffect(effect: Effect) {
const cleanup = effect._cleanup;
effect._cleanup = undefined;
if (typeof cleanup === "function") {
/*@__INLINE__**/ startBatch();
// 항상 컨텍스트 외부에서 정리 함수를 실행하세요.
const prevContext = evalContext;
evalContext = undefined;
try {
cleanup();
} catch (err) {
effect._flags &= ~RUNNING;
effect._flags |= DISPOSED;
disposeEffect(effect);
throw err;
} finally {
evalContext = prevContext;
endBatch();
}
}
}
function disposeEffect(effect: Effect) {
for (
let node = effect._sources;
node !== undefined;
node = node._nextSource
) {
node._source._unsubscribe(node);
}
effect._compute = undefined;
effect._sources = undefined;
cleanupEffect(effect);
}
function endEffect(this: Effect, prevContext?: Computed | Effect) {
if (evalContext !== this) {
throw new Error("Out-of-order effect");
}
cleanupSources(this);
evalContext = prevContext;
this._flags &= ~RUNNING;
if (this._flags & DISPOSED) {
disposeEffect(this);
}
endBatch();
}
type EffectCleanup = () => unknown;
declare class Effect {
_compute?: () => unknown | EffectCleanup;
_cleanup?: () => unknown;
_sources?: Node;
_nextBatchedEffect?: Effect;
_flags: number;
constructor(compute: () => unknown | EffectCleanup);
_callback(): void;
_start(): () => void;
_notify(): void;
_dispose(): void;
}
function Effect(this: Effect, compute: () => unknown | EffectCleanup) {
this._compute = compute;
this._cleanup = undefined;
this._sources = undefined;
this._nextBatchedEffect = undefined;
this._flags = TRACKING;
}
Effect.prototype._callback = function () {
const finish = this._start();
try {
if (this._flags & DISPOSED) return;
if (this._compute === undefined) return;
const cleanup = this._compute();
if (typeof cleanup === "function") {
this._cleanup = cleanup as EffectCleanup;
}
} finally {
finish();
}
};
Effect.prototype._start = function () {
if (this._flags & RUNNING) {
cycleDetected();
}
this._flags |= RUNNING;
this._flags &= ~DISPOSED;
cleanupEffect(this);
prepareSources(this);
/*@__INLINE__**/ startBatch();
const prevContext = evalContext;
evalContext = this;
return endEffect.bind(this, prevContext);
};
Effect.prototype._notify = function () {
if (!(this._flags & NOTIFIED)) {
this._flags |= NOTIFIED;
this._nextBatchedEffect = batchedEffect;
batchedEffect = this;
}
};
Effect.prototype._dispose = function () {
this._flags |= DISPOSED;
if (!(this._flags & RUNNING)) {
disposeEffect(this);
}
};
function effect(compute: () => unknown | EffectCleanup): () => void {
const effect = new Effect(compute);
try {
effect._callback();
} catch (err) {
effect._dispose();
throw err;
}
// `() => effect._dispose()`와 같은 래퍼 대신 바인딩된 함수를 반환합니다.
// 바인딩된 함수도 마찬가지로 빠르고 메모리를 훨씬 적게 차지하는 것처럼 보이기 때문입니다.
return effect._dispose.bind(effect);
}
export {
signal,
computed,
effect,
batch,
Signal,
ReadonlySignal,
untracked,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment