import { expect, test } from "vitest" import type { Effect } from "#Source/reactor/index.ts" import { computed, effect, effectScope, endBatch, reactiveSystem, signal, startBatch, } from "#Source/reactor/index.ts" /** * @description 这个测试文件应该与 https://github.com/stackblitz/alien-signals/blob/master/tests/effect.spec.ts 保持同步。 */ test("should clear subscriptions when untracked by all subscribers", () => { /** * @description A * | * b * | * e */ let bRunTimes = 0 const a = signal(() => 1) const b = computed(() => { bRunTimes = bRunTimes + 1 return a.get() * 2 }) const e = effect(() => { b.get() }) expect(bRunTimes).toBe(1) a.set(2) expect(bRunTimes).toBe(2) e.dispose() a.set(3) expect(bRunTimes).toBe(2) }) test("should not run untracked inner effect", () => { /** * @description A * /\ * b e2(inner) * \ / * e1(outer) * * 解释:当 a 发生变更时,e1 总是会先于 e2 运行,e1 运行时会重新收集依赖,由于 * e2 是在条件分支中创建的,若收集依赖时该条件分支未被运行,依赖收集机制也 * 会将之前 e1 和 e2 之间的依赖关系清除掉,同时 e2 和 a 之间的依赖关系也会 * 被清除掉。 */ let outerEffectTruthyBranchRunTimes = 0 const a = signal(() => 3) const b = computed(() => a.get() > 0) // outer effect effect( () => { if (b.get() === true) { outerEffectTruthyBranchRunTimes = outerEffectTruthyBranchRunTimes + 1 effect( () => { if (a.get() === 0) { throw new Error("bad") } }, { name: "inner effect" }, ) } }, { name: "outer effect" }, ) expect(outerEffectTruthyBranchRunTimes).toBe(1) a.set(2) expect(outerEffectTruthyBranchRunTimes).toBe(1) a.set(1) expect(outerEffectTruthyBranchRunTimes).toBe(1) a.set(0) expect(outerEffectTruthyBranchRunTimes).toBe(1) }) test("should run outer effect first", () => { /** * @description A b * | \ / * | \ / * | e2(inner) * \ / * e1(outer) */ const a = signal(() => 1) const b = signal(() => 1) effect(() => { if (a.get() !== 0) { effect(() => { b.get() if (a.get() === 0) { throw new Error("bad") } }) } }) startBatch() b.set(0) a.set(0) endBatch() }) test("should not trigger inner effect when resolve maybe dirty", () => { /** * @description A * | * b * | * e2(inner) * | * e1(outer) */ const a = signal(() => 0) const b = computed(() => a.get() % 2) let innerTriggerTimes = 0 effect(() => { effect(() => { b.get() innerTriggerTimes = innerTriggerTimes + 1 if (innerTriggerTimes >= 2) { throw new Error("bad") } }) }) expect(innerTriggerTimes).toBe(1) a.set(2) expect(innerTriggerTimes).toBe(1) }) test("should notify inner effects in the same order as non-inner effects", () => { const a = signal(() => 0) const b = signal(() => 0) const c = computed(() => a.get() - b.get()) const order1: string[] = [] const order2: string[] = [] const order3: string[] = [] effect(() => { order1.push("effect1") a.get() }) effect(() => { order1.push("effect2") a.get() b.get() }) effect(() => { c.get() effect(() => { order2.push("effect1") a.get() }) effect(() => { order2.push("effect2") a.get() b.get() }) }) effectScope(() => { effect(() => { order3.push("effect1") a.get() }) effect(() => { order3.push("effect2") a.get() b.get() }) }) order1.length = 0 order2.length = 0 order3.length = 0 startBatch() b.set(1) a.set(1) endBatch() expect(order1).toEqual(["effect2", "effect1"]) expect(order2).toEqual(order1) expect(order3).toEqual(order1) }) test("should custom effect support batch", () => { const batchEffect = (fn: () => void): Effect => { return effect(() => { startBatch() try { return fn() } finally { endBatch() } }) } const logs: string[] = [] const a = signal(() => 0) const b = signal(() => 0) const aa = computed(() => { logs.push("aa-0") if (!a.get()) { b.set(1) } logs.push("aa-1") }) const bb = computed(() => { logs.push("bb") return b.get() }) batchEffect(() => { bb.get() }) batchEffect(() => { aa.get() }) expect(logs).toEqual(["bb", "aa-0", "aa-1", "bb"]) }) test("should duplicate subscribers do not affect the notify order", () => { const srcA = signal(() => 0, { name: "srcA" }) const srcB = signal(() => 0, { name: "srcB" }) const order: string[] = [] effect( () => { order.push("a") reactiveSystem.setNoActiveNodeAsSub() const isOne = srcB.get() === 1 reactiveSystem.resetActiveNodeAsSub() if (isOne) { srcA.get() } srcB.get() srcA.get() }, { name: "effect-a" }, ) effect( () => { order.push("b") srcA.get() }, { name: "effect-b" }, ) srcB.set(1) expect(order).toEqual(["a", "b", "a"]) order.length = 0 srcA.set(srcA.get() + 1) expect(order).toEqual(["a", "b"]) }) test("should handle side effect with inner effects", () => { const a = signal(() => 0) const b = signal(() => 0) const order: string[] = [] effect(() => { effect(() => { a.get() order.push("a") }) effect(() => { b.get() order.push("b") }) expect(order).toEqual(["a", "b"]) order.length = 0 b.set(1) a.set(1) expect(order).toEqual(["b", "a"]) }) }) test("should handle flags are indirectly updated during resolve pending", () => { /** * @description A * | * b * /\ * c | * \ / * d * | * e */ const a = signal(() => false) const b = computed(() => a.get()) const c = computed(() => { b.get() return 0 }) const d = computed(() => { c.get() return b.get() }) let effectRunTimes = 0 effect(() => { d.get() effectRunTimes = effectRunTimes + 1 }) expect(effectRunTimes).toBe(1) a.set(true) expect(effectRunTimes).toBe(2) }) test("should handle effect recursion for the first execution", () => { const src1 = signal(() => 0) const src2 = signal(() => 0) let effectARunTimes = 0 let effectBRunTimes = 0 effect(() => { effectARunTimes = effectARunTimes + 1 src1.set(Math.min(src1.get() + 1, 5)) }) effect(() => { effectBRunTimes = effectBRunTimes + 1 src2.set(Math.min(src2.get() + 1, 5)) src2.get() }) expect(effectARunTimes).toBe(1) expect(effectBRunTimes).toBe(1) }) test("should support custom recurse effect", () => { const src = signal(() => 0) let effectRunTimes = 0 effect(() => { reactiveSystem.getActiveNodeAsSub()!.flags.unsetTracking() effectRunTimes = effectRunTimes + 1 src.set(Math.min(src.get() + 1, 5)) }) expect(effectRunTimes).toBe(6) }) test("should not execute skipped effects from previous failed flush when updating unrelated signal", () => { /** * @description A c b * * | | | * * / | \ d | / | \ / | e1 e2 e3 e4 */ const a = signal(() => 0, { name: "a" }) const b = signal(() => 0, { name: "b" }) const c = signal(() => 0, { name: "c" }) const d = computed(() => c.get(), { name: "d" }) let effect3Executed = false effect( () => { a.get() }, { name: "effect1" }, ) effect( () => { if (a.get() === 2) { throw new Error("Error in effect 2") } }, { name: "effect2" }, ) effect( () => { a.get() d.get() effect3Executed = true }, { name: "effect3" }, ) effect( () => { b.get() }, { name: "effect4" }, ) expect(effect3Executed).toBe(true) a.set(1) expect(effect3Executed).toBe(true) effect3Executed = false try { a.set(2) } catch (exception) { expect((exception as Error).message).toBe("Error in effect 2") } expect(effect3Executed).toBe(false) b.set(1) expect(effect3Executed).toBe(false) c.set(1) expect(effect3Executed).toBe(true) })