├── .vscode
└── launch.json
├── LICENSE
├── bit.lua
├── utils.lua
├── tests
├── test_effectScope.lua
├── test_computed.lua
├── test_untrack.lua
├── test_trigger.lua
├── test_issue_48.lua
├── test_recursion.lua
├── test_effect.lua
├── test_nil_value.lua
└── test_topology.lua
├── example_shopping_cart.lua
├── README.md
├── README.en.md
└── alien_signals.lua
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "lua",
9 | "request": "launch",
10 | "tag": "independent_file",
11 | "name": "LuaPanda-IndependentFile",
12 | "description": "独立文件调试模式,使用前请参考文档",
13 | "luaPath": "",
14 | "packagePath": [],
15 | "luaFileExtension": "",
16 | "connectionPort": 8820,
17 | "stopOnEntry": true,
18 | "useCHook": true
19 | },
20 | ]
21 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 YanqingXu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bit.lua:
--------------------------------------------------------------------------------
1 | local bit = {}
2 |
3 | function bit.lshift(a, n)
4 | return a * (2 ^ n)
5 | end
6 |
7 | function bit.rshift(a, n)
8 | return math.floor(a / (2 ^ n))
9 | end
10 |
11 | function bit.band(a, b)
12 | local result = 0
13 | local bitval = 1
14 | while a > 0 and b > 0 do
15 | if a % 2 == 1 and b % 2 == 1 then
16 | result = result + bitval
17 | end
18 | bitval = bitval * 2
19 | a = math.floor(a/2)
20 | b = math.floor(b/2)
21 | end
22 | return result
23 | end
24 |
25 | function bit.bor(a, b)
26 | local result = 0
27 | local bitval = 1
28 | while a > 0 or b > 0 do
29 | if a % 2 == 1 or b % 2 == 1 then
30 | result = result + bitval
31 | end
32 | bitval = bitval * 2
33 | a = math.floor(a/2)
34 | b = math.floor(b/2)
35 | end
36 | return result
37 | end
38 |
39 | function bit.bxor(a, b)
40 | local result = 0
41 | local value = 1
42 | while a > 0 or b > 0 do
43 | local aa = a % 2
44 | local bb = b % 2
45 | if aa ~= bb then
46 | result = result + value
47 | end
48 | a = math.floor(a / 2)
49 | b = math.floor(b / 2)
50 | value = value * 2
51 | end
52 | return result
53 | end
54 |
55 | function bit.bnot(a)
56 | return 4294967295 - a
57 | end
58 |
59 | return bit
--------------------------------------------------------------------------------
/utils.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | * Utils Module - Testing utilities for the reactive system
3 | * 工具模块 - 响应式系统的测试工具
4 | *
5 | * This module provides testing utilities for the reactive system's test files,
6 | * including test runner and assertion functions for unit testing.
7 | * 该模块为响应式系统的测试文件提供测试工具,
8 | * 包括测试运行器和用于单元测试的断言函数。
9 | ]]
10 |
11 | local utils = {}
12 |
13 | --[[
14 | * Simple test runner utility
15 | * 简单的测试运行器工具
16 | *
17 | * @param name: Test name to display / 要显示的测试名称
18 | * @param fn: Test function to execute / 要执行的测试函数
19 | *
20 | * This provides a basic testing framework for the reactive system's test files.
21 | * It simply prints the test name and executes the test function.
22 | * 这为响应式系统的测试文件提供了基本的测试框架。
23 | * 它简单地打印测试名称并执行测试函数。
24 | ]]
25 | function utils.test(name, fn)
26 | print(name)
27 | fn()
28 | end
29 |
30 | --[[
31 | * Create an expectation object for testing assertions
32 | * 创建用于测试断言的期望对象
33 | *
34 | * @param actual: The actual value to test / 要测试的实际值
35 | * @return: Expectation object with assertion methods / 带有断言方法的期望对象
36 | *
37 | * This provides a Jest-like testing API for the reactive system's tests.
38 | * It supports basic equality assertions for both primitive values and objects.
39 | * The returned object has methods like toBe() and toEqual() for different types of comparisons.
40 | *
41 | * 这为响应式系统的测试提供了类似 Jest 的测试 API。
42 | * 它支持原始值和对象的基本相等断言。
43 | * 返回的对象具有 toBe() 和 toEqual() 等方法用于不同类型的比较。
44 | ]]
45 | function utils.expect(actual)
46 | return {
47 | -- Strict equality assertion (===)
48 | -- 严格相等断言 (===)
49 | toBe = function(expected)
50 | assert(actual == expected)
51 | end,
52 |
53 | -- Deep equality assertion for objects and primitive values
54 | -- 对象和原始值的深度相等断言
55 | toEqual = function(expected)
56 | if type(actual) == "table" and type(expected) == "table" then
57 | for k, v in pairs(expected) do
58 | assert(actual[k] == v)
59 | end
60 | else
61 | assert(actual == expected)
62 | end
63 | end,
64 | }
65 | end
66 |
67 | return utils
--------------------------------------------------------------------------------
/tests/test_effectScope.lua:
--------------------------------------------------------------------------------
1 | -- test_effectScope.lua
2 | -- Test for Lua implementation of reactive system - focusing on effect scope functionality
3 | print("========== Reactive System Effect Scope Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local reactive = require("alien_signals")
7 | local signal = reactive.signal
8 | local effect = reactive.effect
9 | local effectScope = reactive.effectScope
10 |
11 |
12 | local utils = require("utils")
13 | local test = utils.test
14 | local expect = utils.expect
15 |
16 | test('should not trigger after stop', function ()
17 | local count = signal(1)
18 |
19 | local triggers = 0
20 | local effect1 = nil
21 |
22 | local stopScope = effectScope(function()
23 | effect1 = effect(function()
24 | triggers = triggers + 1
25 | count()
26 | end)
27 | expect(triggers).toBe(1)
28 |
29 | count(2)
30 | expect(triggers).toBe(2)
31 | end)
32 |
33 | count(3)
34 | expect(triggers).toBe(3)
35 | stopScope()
36 | count(4)
37 | expect(triggers).toBe(3)
38 |
39 | print("test passed\n")
40 | end)
41 |
42 | test('should dispose inner effects if created in an effect', function()
43 | local source = signal(1)
44 |
45 | local triggers = 0
46 |
47 | effect(function()
48 | local dispose = effectScope(function()
49 | effect(function()
50 | source()
51 | triggers = triggers + 1
52 | end)
53 | end)
54 | expect(triggers).toBe(1)
55 |
56 | source(2)
57 | expect(triggers).toBe(2)
58 | dispose()
59 | source(3)
60 | expect(triggers).toBe(2)
61 |
62 | print("test passed\n")
63 | end)
64 |
65 | test('should track signal updates in an inner scope when accessed by an outer effect', function()
66 | local source = signal(1)
67 |
68 | local triggers = 0
69 |
70 | effect(function()
71 | effectScope(function()
72 | source()
73 | end)
74 | triggers = triggers + 1
75 | end)
76 |
77 | expect(triggers).toBe(1)
78 | source(2)
79 | expect(triggers).toBe(2)
80 | end)
81 |
82 | print("test passed\n")
83 | end)
84 |
85 | print("========== All tests passed!!! ==========\n")
86 | print("====================================================\n")
87 |
88 |
--------------------------------------------------------------------------------
/tests/test_computed.lua:
--------------------------------------------------------------------------------
1 | -- test_computed.lua
2 | -- Test for Lua implementation of reactive system - focusing on effect functionality
3 | print("========== Reactive System Computed Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local utils = require("utils")
7 | local test = utils.test
8 | local expect = utils.expect
9 |
10 | local reactive = require("alien_signals")
11 | local signal = reactive.signal
12 | local computed = reactive.computed
13 |
14 | test('should correctly propagate changes through computed signals', function ()
15 | local src = signal(0)
16 | local c1 = computed(function() return src() % 2 end)
17 | local c2 = computed(function() return c1() end)
18 | local c3 = computed(function() return c2() end)
19 |
20 | c3()
21 | src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
22 | c2() -- c1 -> none, c2 -> none
23 | src(3) -- c1 -> dirty, c2 -> toCheckDirty
24 |
25 | expect(c3()).toBe(1)
26 | print("test passed\n")
27 | end)
28 |
29 | test('should propagate updated source value through chained computations', function ()
30 | local src = signal(0)
31 | local a = computed(function() return src() end)
32 | local b = computed(function() return a() % 2 end)
33 | local c = computed(function() return src() end)
34 | local d = computed(function() return b() + c() end)
35 |
36 | expect(d()).toBe(0)
37 | src(2)
38 | expect(d()).toBe(2)
39 | print("test passed\n")
40 | end)
41 |
42 | test('should handle flags are indirectly updated during checkDirty', function ()
43 | local a = signal(false)
44 | local b = computed(function() return a() end)
45 | local c = computed(function()
46 | b()
47 | return 0
48 | end)
49 | local d = computed(function()
50 | c()
51 | return b()
52 | end)
53 |
54 | expect(d()).toBe(false)
55 | a(true)
56 | expect(d()).toBe(true)
57 | print("test passed\n")
58 | end)
59 |
60 | test('should not update if the signal value is reverted', function ()
61 | local times = 0
62 |
63 | local src = signal(0)
64 | local c1 = computed(function()
65 | times = times + 1
66 | return src()
67 | end)
68 | c1()
69 | expect(times).toBe(1)
70 | src(1)
71 | src(0)
72 | c1()
73 | expect(times).toBe(1)
74 |
75 | print("test passed\n")
76 | end)
77 |
78 | print("========== All tests passed!!! ==========\n")
79 | print("====================================================\n")
--------------------------------------------------------------------------------
/tests/test_untrack.lua:
--------------------------------------------------------------------------------
1 | -- test_untrack.lua
2 | -- Test for Lua implementation of reactive system - focusing on untrack functionality
3 | print("========== Reactive System Untrack Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local reactive = require("alien_signals")
7 | local signal = reactive.signal
8 | local computed = reactive.computed
9 | local effect = reactive.effect
10 | local effectScope = reactive.effectScope
11 | local setActiveSub = reactive.setActiveSub
12 |
13 | local utils = require("utils")
14 | local test = utils.test
15 | local expect = utils.expect
16 |
17 | test('should pause tracking in computed', function()
18 | local src = signal(0)
19 |
20 | local computedTriggerTimes = 0
21 | local c = computed(function()
22 | computedTriggerTimes = computedTriggerTimes + 1
23 | local currentSub = setActiveSub(nil)
24 | local value = src()
25 | setActiveSub(currentSub)
26 | return value
27 | end)
28 |
29 | expect(c()).toBe(0)
30 | expect(computedTriggerTimes).toBe(1)
31 |
32 | src(1)
33 | src(2)
34 | src(3)
35 | expect(c()).toBe(0)
36 | expect(computedTriggerTimes).toBe(1)
37 | print("test passed\n")
38 | end)
39 |
40 | test('should pause tracking in effect', function()
41 | local src = signal(0)
42 | local is = signal(0)
43 |
44 | local effectTriggerTimes = 0
45 | effect(function()
46 | effectTriggerTimes = effectTriggerTimes + 1
47 | if is() ~= 0 then
48 | local currentSub = setActiveSub(nil)
49 | src()
50 | setActiveSub(currentSub)
51 | end
52 | end)
53 |
54 | expect(effectTriggerTimes).toBe(1)
55 |
56 | is(1)
57 | expect(effectTriggerTimes).toBe(2)
58 |
59 | src(1)
60 | src(2)
61 | src(3)
62 | expect(effectTriggerTimes).toBe(2)
63 |
64 | is(2)
65 | expect(effectTriggerTimes).toBe(3)
66 |
67 | src(4)
68 | src(5)
69 | src(6)
70 | expect(effectTriggerTimes).toBe(3)
71 |
72 | is(0)
73 | expect(effectTriggerTimes).toBe(4)
74 |
75 | src(7)
76 | src(8)
77 | src(9)
78 | expect(effectTriggerTimes).toBe(4)
79 | print("test passed\n")
80 | end)
81 |
82 | test('should pause tracking in effect scope', function()
83 | local src = signal(0)
84 |
85 | local effectTriggerTimes = 0
86 | effectScope(function()
87 | effect(function()
88 | effectTriggerTimes = effectTriggerTimes + 1
89 | local currentSub = setActiveSub(nil)
90 | src()
91 | setActiveSub(currentSub)
92 | end)
93 | end)
94 |
95 | expect(effectTriggerTimes).toBe(1)
96 |
97 | src(1)
98 | src(2)
99 | src(3)
100 | expect(effectTriggerTimes).toBe(1)
101 | print("test passed\n")
102 | end)
103 |
104 | print("========== All Untrack Tests Completed ==========\n")
105 |
--------------------------------------------------------------------------------
/tests/test_trigger.lua:
--------------------------------------------------------------------------------
1 | -- test_trigger.lua
2 | -- Test for Lua implementation of reactive system - focusing on trigger functionality
3 | print("========== Reactive System Trigger Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local utils = require("utils")
7 | local test = utils.test
8 | local expect = utils.expect
9 |
10 | local reactive = require("alien_signals")
11 | local signal = reactive.signal
12 | local computed = reactive.computed
13 | local effect = reactive.effect
14 | local trigger = reactive.trigger
15 |
16 | test('should not throw when triggering with no dependencies', function()
17 | local success = pcall(function()
18 | trigger(function() end)
19 | end)
20 | expect(success).toBe(true)
21 | print("test passed\n")
22 | end)
23 |
24 | test('should trigger updates for dependent computed signals', function()
25 | local arr = signal({})
26 | local length = computed(function()
27 | local a = arr()
28 | return #a
29 | end)
30 |
31 | expect(length()).toBe(0)
32 |
33 | table.insert(arr(), 1)
34 | expect(length()).toBe(0)
35 |
36 | trigger(arr)
37 | expect(length()).toBe(1)
38 |
39 | print("test passed\n")
40 | end)
41 |
42 | test('should trigger updates for the second source signal', function()
43 | local src1 = signal({})
44 | local src2 = signal({})
45 | local length = computed(function()
46 | return #src2()
47 | end)
48 |
49 | expect(length()).toBe(0)
50 |
51 | table.insert(src2(), 1)
52 |
53 | trigger(function()
54 | src1()
55 | src2()
56 | end)
57 |
58 | expect(length()).toBe(1)
59 |
60 | print("test passed\n")
61 | end)
62 |
63 | test('should trigger effect once', function()
64 | local src1 = signal({})
65 | local src2 = signal({})
66 |
67 | local triggers = 0
68 |
69 | effect(function()
70 | triggers = triggers + 1
71 | src1()
72 | src2()
73 | end)
74 |
75 | expect(triggers).toBe(1)
76 |
77 | trigger(function()
78 | src1()
79 | src2()
80 | end)
81 |
82 | expect(triggers).toBe(2)
83 |
84 | print("test passed\n")
85 | end)
86 |
87 | test('should not notify the trigger function sub', function()
88 | local src1 = signal({})
89 | local src2 = computed(function()
90 | return src1()
91 | end)
92 |
93 | effect(function()
94 | src1()
95 | src2()
96 | end)
97 |
98 | -- This should not throw an error or cause infinite recursion
99 | -- 这不应该抛出错误或导致无限递归
100 | local success = pcall(function()
101 | trigger(function()
102 | src1()
103 | src2()
104 | end)
105 | end)
106 |
107 | expect(success).toBe(true)
108 |
109 | print("test passed\n")
110 | end)
111 |
112 | print("========== All tests passed!!! ==========\n")
113 | print("====================================================\n")
114 |
115 |
--------------------------------------------------------------------------------
/tests/test_issue_48.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | * Test for issue #48 regression
3 | * 测试问题 #48 的回归测试
4 | *
5 | * This test verifies a specific edge case where disposing an inner effect
6 | * created within a reaction should not cause issues.
7 | * 此测试验证一个特定的边缘情况,即在反应中创建的内部副作用被释放时不应导致问题。
8 | ]]
9 |
10 | print("========== Issue #48 Regression Test ==========\n")
11 |
12 | local reactive = require("alien_signals")
13 | local signal = reactive.signal
14 | local computed = reactive.computed
15 | local effect = reactive.effect
16 | local setActiveSub = reactive.setActiveSub
17 |
18 | local utils = require("utils")
19 | local test = utils.test
20 | local expect = utils.expect
21 |
22 | --[[
23 | * Helper function: untracked
24 | * 辅助函数:不追踪
25 | *
26 | * Executes a callback without tracking dependencies
27 | * 执行回调而不追踪依赖
28 | ]]
29 | local function untracked(callback)
30 | local currentSub = setActiveSub(nil)
31 | local success, result = pcall(callback)
32 | setActiveSub(currentSub)
33 | if not success then
34 | error(result)
35 | end
36 | return result
37 | end
38 |
39 | --[[
40 | * Helper function: reaction
41 | * 辅助函数:反应
42 | *
43 | * Creates a reaction that watches a data function and executes an effect function
44 | * 创建一个反应,监视数据函数并执行副作用函数
45 | ]]
46 | local function reaction(dataFn, effectFn, options)
47 | options = options or {}
48 | local scheduler = options.scheduler or function(fn) fn() end
49 | local equals = options.equals or function(a, b) return a == b end
50 | local onError = options.onError
51 | local once = options.once or false
52 | local fireImmediately = options.fireImmediately or false
53 |
54 | local prevValue
55 | local version = 0
56 |
57 | local tracked = computed(function()
58 | local success, result = pcall(dataFn)
59 | if not success then
60 | untracked(function()
61 | if onError then onError(result) end
62 | end)
63 | return prevValue
64 | end
65 | return result
66 | end)
67 |
68 | local dispose = nil
69 | dispose = effect(function()
70 | local current = tracked()
71 | if not fireImmediately and version == 0 then
72 | prevValue = current
73 | end
74 | version = version + 1
75 | if equals(current, prevValue) then return end
76 | local oldValue = prevValue
77 | prevValue = current
78 | untracked(function()
79 | scheduler(function()
80 | local success, err = pcall(function()
81 | effectFn(current, oldValue)
82 | end)
83 | if not success then
84 | if onError then onError(err) end
85 | end
86 | if once then
87 | if fireImmediately and version > 1 then
88 | dispose()
89 | elseif not fireImmediately and version > 0 then
90 | dispose()
91 | end
92 | end
93 | end)
94 | end)
95 | end)
96 |
97 | return dispose
98 | end
99 |
100 | test('#48 - disposing inner effect in reaction should not cause issues', function()
101 | local source = signal(0)
102 | local disposeInner
103 |
104 | reaction(
105 | function() return source() end,
106 | function(val)
107 | if val == 1 then
108 | disposeInner = reaction(
109 | function() return source() end,
110 | function() end
111 | )
112 | elseif val == 2 then
113 | disposeInner()
114 | end
115 | end
116 | )
117 |
118 | -- This sequence should not cause any errors
119 | -- 这个序列不应导致任何错误
120 | source(1)
121 | source(2)
122 | source(3)
123 |
124 | print("test passed\n")
125 | end)
126 |
127 | print("========== Issue #48 Test Passed ==========\n")
128 | print("====================================================\n")
129 |
130 |
--------------------------------------------------------------------------------
/tests/test_recursion.lua:
--------------------------------------------------------------------------------
1 | -- test_recursion.lua
2 | -- Tests for recursion prevention in effects and computed values
3 | -- 副作用和计算值的递归防止测试
4 | print("========== Reactive System Recursion Prevention Tests ==========\n")
5 |
6 | -- Load reactive system
7 | local utils = require("utils")
8 | local test = utils.test
9 | local expect = utils.expect
10 |
11 | local reactive = require("alien_signals")
12 | local signal = reactive.signal
13 | local computed = reactive.computed
14 | local effect = reactive.effect
15 |
16 | test('should not trigger effect itself during first run', function()
17 | local s = signal(0)
18 | local runCount = 0
19 |
20 | effect(function()
21 | runCount = runCount + 1
22 | local val = s()
23 | -- This would cause recursion without RecursedCheck
24 | -- 如果没有 RecursedCheck,这会导致递归
25 | if runCount == 1 then
26 | s(val + 1)
27 | end
28 | end)
29 |
30 | -- Effect should run only once during initialization
31 | -- 副作用在初始化期间应该只运行一次
32 | expect(runCount).toBe(1)
33 | expect(s()).toBe(1)
34 |
35 | print("test passed\n")
36 | end)
37 |
38 | test('should not trigger computed itself during first run', function()
39 | local s = signal(0)
40 | local computeCount = 0
41 |
42 | local c = computed(function()
43 | computeCount = computeCount + 1
44 | local val = s()
45 | -- This would cause recursion without RecursedCheck
46 | -- 如果没有 RecursedCheck,这会导致递归
47 | if computeCount == 1 then
48 | s(val + 1)
49 | end
50 | return val
51 | end)
52 |
53 | -- Access computed to trigger first run
54 | -- 访问计算值以触发首次运行
55 | local result = c()
56 |
57 | expect(computeCount).toBe(1)
58 | expect(result).toBe(0)
59 | expect(s()).toBe(1)
60 |
61 | print("test passed\n")
62 | end)
63 |
64 | test('should not trigger effect itself during execution even after first run', function()
65 | local s = signal(0)
66 | local runCount = 0
67 |
68 | effect(function()
69 | runCount = runCount + 1
70 | local val = s()
71 | -- Even after first run, modifying the signal during execution
72 | -- will not trigger the effect again (RecursedCheck prevents it)
73 | -- 即使在首次运行后,在执行期间修改信号也不会再次触发副作用
74 | if runCount == 2 then
75 | s(val + 1)
76 | end
77 | end)
78 |
79 | expect(runCount).toBe(1)
80 |
81 | -- Trigger the effect
82 | -- 触发副作用
83 | s(1)
84 |
85 | -- Effect should run twice: once for s(1), and the s(val+1) inside
86 | -- does not trigger it again because RecursedCheck is set
87 | -- 副作用应该运行两次:一次为 s(1),内部的 s(val+1) 不会再次触发它
88 | expect(runCount).toBe(2)
89 | expect(s()).toBe(2)
90 |
91 | print("test passed\n")
92 | end)
93 |
94 | test('should not cause recursion with nested effects during initialization', function()
95 | local s1 = signal(0)
96 | local s2 = signal(0)
97 | local runCount1 = 0
98 | local runCount2 = 0
99 |
100 | effect(function()
101 | runCount1 = runCount1 + 1
102 | local val = s1()
103 |
104 | effect(function()
105 | runCount2 = runCount2 + 1
106 | s2()
107 | if runCount2 == 1 then
108 | s1(val + 1)
109 | end
110 | end)
111 | end)
112 |
113 | -- Both effects should run once during initialization
114 | -- 两个副作用在初始化期间都应该只运行一次
115 | expect(runCount1).toBe(1)
116 | expect(runCount2).toBe(1)
117 | expect(s1()).toBe(1)
118 |
119 | print("test passed\n")
120 | end)
121 |
122 | test('should not cause recursion with multiple signals in effect', function()
123 | local s1 = signal(0)
124 | local s2 = signal(0)
125 | local runCount = 0
126 |
127 | effect(function()
128 | runCount = runCount + 1
129 | local val1 = s1()
130 | local val2 = s2()
131 |
132 | if runCount == 1 then
133 | s1(val1 + 1)
134 | s2(val2 + 1)
135 | end
136 | end)
137 |
138 | expect(runCount).toBe(1)
139 | expect(s1()).toBe(1)
140 | expect(s2()).toBe(1)
141 |
142 | print("test passed\n")
143 | end)
144 |
145 | print("========== All tests passed!!! ==========\n")
146 | print("====================================================\n")
147 |
148 |
--------------------------------------------------------------------------------
/example_shopping_cart.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | * Shopping Cart Reactive System Example
3 | * 购物车响应式系统示例
4 | *
5 | * This example demonstrates a complex reactive system with multi-level dependencies,
6 | * batch updates, and side effects as described in the WIKI documentation.
7 | * 这个示例展示了一个复杂的响应式系统,具有多层依赖、批量更新和副作用,
8 | * 如 WIKI 文档中所述。
9 | ]]
10 |
11 | local reactive = require("alien_signals")
12 | local signal = reactive.signal
13 | local computed = reactive.computed
14 | local effect = reactive.effect
15 |
16 | print("=== Shopping Cart Reactive System Demo ===\n")
17 |
18 | -- 1. Basic data signals / 基础数据信号
19 | print("1. Creating basic signals")
20 | local itemPrice = signal(100) -- Item unit price / 商品单价
21 | local quantity = signal(2) -- Item quantity / 商品数量
22 | local discountRate = signal(0.1) -- Discount rate / 折扣率
23 | local taxRate = signal(0.08) -- Tax rate / 税率
24 |
25 | -- 2. First-level computed values / 第一层计算值
26 | print("2. Creating first-level computeds")
27 | local subtotal = computed(function()
28 | print(" -> Computing subtotal")
29 | return itemPrice() * quantity()
30 | end)
31 |
32 | local discountAmount = computed(function()
33 | print(" -> Computing discountAmount")
34 | return subtotal() * discountRate()
35 | end)
36 |
37 | -- 3. Second-level computed values / 第二层计算值
38 | print("3. Creating second-level computeds")
39 | local afterDiscount = computed(function()
40 | print(" -> Computing afterDiscount")
41 | return subtotal() - discountAmount()
42 | end)
43 |
44 | local taxAmount = computed(function()
45 | print(" -> Computing taxAmount")
46 | return afterDiscount() * taxRate()
47 | end)
48 |
49 | -- 4. Final computed value / 最终计算值
50 | print("4. Creating final computed")
51 | local finalTotal = computed(function()
52 | print(" -> Computing finalTotal")
53 | return afterDiscount() + taxAmount()
54 | end)
55 |
56 | -- 5. Side effect: UI updates / 副作用:UI 更新
57 | print("5. Creating UI effect")
58 | local uiUpdateCount = 0
59 | local stopUIEffect = effect(function()
60 | local total = finalTotal()
61 | uiUpdateCount = uiUpdateCount + 1
62 | print(string.format(" [UI] Update #%d - Total: $%.2f",
63 | uiUpdateCount, total))
64 | end)
65 |
66 | -- 6. Side effect: Logging / 副作用:日志记录
67 | print("6. Creating log effect")
68 | local stopLogEffect = effect(function()
69 | print(string.format(" [LOG] Subtotal: $%.2f, Discount: $%.2f",
70 | subtotal(), discountAmount()))
71 | end)
72 |
73 | print("\n=== Initialization Complete ===")
74 | print("Initial state:")
75 | print(string.format(" Item Price: $%.2f", itemPrice()))
76 | print(string.format(" Quantity: %d", quantity()))
77 | print(string.format(" Discount Rate: %.1f%%", discountRate() * 100))
78 | print(string.format(" Tax Rate: %.1f%%", taxRate() * 100))
79 |
80 | -- Test single update / 测试单个更新
81 | print("\n=== Test 1: Single Update - Quantity Change ===")
82 | print("Updating quantity from 2 to 3")
83 | quantity(3)
84 |
85 | -- Test batch update / 测试批量更新
86 | print("\n=== Test 2: Batch Update - Price and Discount ===")
87 | print("Starting batch update")
88 | reactive.startBatch()
89 |
90 | print("Updating item price from $100 to $120")
91 | itemPrice(120)
92 |
93 | print("Updating discount rate from 10% to 15%")
94 | discountRate(0.15)
95 |
96 | print("Ending batch update (effects will run now)")
97 | reactive.endBatch()
98 |
99 | -- Test multiple quick updates / 测试多个快速更新
100 | print("\n=== Test 3: Multiple Quick Updates ===")
101 | print("Testing rapid updates with batching")
102 |
103 | reactive.startBatch()
104 | print("Batch: quantity 3->4->5, price $120->$110->$130")
105 | quantity(4)
106 | quantity(5)
107 | itemPrice(110)
108 | itemPrice(130)
109 | reactive.endBatch()
110 |
111 | -- Final state / 最终状态
112 | print("\n=== Final State Summary ===")
113 | print(string.format("Item Price: $%.2f", itemPrice()))
114 | print(string.format("Quantity: %d", quantity()))
115 | print(string.format("Subtotal: $%.2f", subtotal()))
116 | print(string.format("Discount (%.1f%%): $%.2f",
117 | discountRate() * 100, discountAmount()))
118 | print(string.format("After Discount: $%.2f", afterDiscount()))
119 | print(string.format("Tax (%.1f%%): $%.2f",
120 | taxRate() * 100, taxAmount()))
121 | print(string.format("Final Total: $%.2f", finalTotal()))
122 | print(string.format("Total UI Updates: %d", uiUpdateCount))
123 |
124 | -- Cleanup / 清理
125 | print("\n=== Cleanup ===")
126 | print("Stopping effects")
127 | stopUIEffect()
128 | stopLogEffect()
129 |
130 | print("\nDemo completed!")
131 |
--------------------------------------------------------------------------------
/tests/test_effect.lua:
--------------------------------------------------------------------------------
1 | -- test_effect.lua
2 | -- Test for Lua implementation of reactive system - focusing on effect functionality
3 | print("========== Reactive System Effect Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local reactive = require("alien_signals")
7 | local signal = reactive.signal
8 | local computed = reactive.computed
9 | local effect = reactive.effect
10 | local effectScope = reactive.effectScope
11 | local startBatch = reactive.startBatch
12 | local endBatch = reactive.endBatch
13 | local setActiveSub = reactive.setActiveSub
14 |
15 | local utils = require("utils")
16 | local test = utils.test
17 | local expect = utils.expect
18 | local bit = require("bit")
19 |
20 |
21 | test('should clear subscriptions when untracked by all subscribers', function ()
22 | local bRunTimes = 0
23 |
24 | local a = signal(1)
25 | local b = computed(function()
26 | bRunTimes = bRunTimes + 1
27 | return a() * 2
28 | end)
29 |
30 | local stopEffect = effect(function()
31 | b()
32 | end)
33 | expect(bRunTimes).toBe(1)
34 | a(2)
35 | expect(bRunTimes).toBe(2)
36 | stopEffect()
37 | a(3)
38 | expect(bRunTimes).toBe(2)
39 | print("test passed\n")
40 | end)
41 |
42 | test('should not run untracked inner effect', function ()
43 | local a = signal(3)
44 | local b = computed(function()
45 | return a() > 0
46 | end)
47 |
48 | effect(function()
49 | if b() then
50 | effect(function()
51 | if a() == 0 then
52 | error("bad")
53 | end
54 | end)
55 | end
56 | end)
57 |
58 | a(2)
59 | a(1)
60 | a(0)
61 | print("test passed\n")
62 | end)
63 |
64 | test('should run outer effect first', function ()
65 | local a = signal(1)
66 | local b = signal(1)
67 |
68 | effect(function()
69 | if a() ~= 0 then
70 | effect(function()
71 | b()
72 | if a() == 0 then
73 | error("bad")
74 | end
75 | end)
76 | else
77 | end
78 | end)
79 |
80 | startBatch()
81 | b(0)
82 | a(0)
83 | endBatch()
84 | print("test passed\n")
85 | end)
86 |
87 | test('should not trigger inner effect when resolve maybe dirty', function ()
88 | local a = signal(0)
89 | local b = computed(function() return a() % 2 end)
90 |
91 | local innerTriggerTimes = 0
92 |
93 | effect(function()
94 | effect(function()
95 | b()
96 | innerTriggerTimes = innerTriggerTimes + 1
97 | if innerTriggerTimes >= 2 then
98 | error("bad")
99 | end
100 | end)
101 | end)
102 |
103 | a(2)
104 | print("test passed\n")
105 | end)
106 |
107 | test('should notify inner effects in the same order as non-inner effects', function()
108 | local a = signal(0)
109 | local b = signal(0)
110 | local c = computed(function() return a() - b() end)
111 | local order1 = {}
112 | local order2 = {}
113 | local order3 = {}
114 |
115 | effect(function()
116 | table.insert(order1, 'effect1')
117 | a()
118 | end)
119 | effect(function()
120 | table.insert(order1, 'effect2')
121 | a()
122 | b()
123 | end)
124 |
125 | effect(function()
126 | c()
127 | effect(function()
128 | table.insert(order2, 'effect1')
129 | a()
130 | end)
131 | effect(function()
132 | table.insert(order2, 'effect2')
133 | a()
134 | b()
135 | end)
136 | end)
137 |
138 | effectScope(function()
139 | effect(function()
140 | table.insert(order3, 'effect1')
141 | a()
142 | end)
143 | effect(function()
144 | table.insert(order3, 'effect2')
145 | a()
146 | b()
147 | end)
148 | end)
149 |
150 | order1 = {}
151 | order2 = {}
152 | order3 = {}
153 |
154 | startBatch()
155 | b(1)
156 | a(1)
157 | endBatch()
158 |
159 | expect(order1).toEqual({'effect2', 'effect1'})
160 | expect(order2).toEqual(order1)
161 | expect(order3).toEqual(order1)
162 | print("test passed\n")
163 | end)
164 |
165 | test('should custom effect support batch', function()
166 | local function batchEffect(fn)
167 | return effect(function()
168 | startBatch()
169 | local success, result = pcall(fn)
170 | endBatch()
171 | if success then
172 | return result
173 | end
174 | end)
175 | end
176 |
177 | local logs = {}
178 | local a = signal(0)
179 | local b = signal(0)
180 |
181 | local aa = computed(function()
182 | table.insert(logs, 'aa-0')
183 | if a() == 0 then -- Lua: a() == 0 is equivalent to TypeScript: !a()
184 | b(1)
185 | end
186 | table.insert(logs, 'aa-1')
187 | end)
188 |
189 | local bb = computed(function()
190 | table.insert(logs, 'bb')
191 | return b()
192 | end)
193 |
194 | batchEffect(function()
195 | bb()
196 | end)
197 | batchEffect(function()
198 | aa()
199 | end)
200 |
201 | expect(logs).toEqual({'bb', 'aa-0', 'aa-1', 'bb'})
202 | print("test passed\n")
203 | end)
204 |
205 | test('should duplicate subscribers do not affect the notify order', function()
206 | local src1 = signal(0)
207 | local src2 = signal(0)
208 | local order = {}
209 |
210 | effect(function()
211 | table.insert(order, 'a')
212 | local currentSub = setActiveSub(nil)
213 | local isOne = src2() == 1
214 | setActiveSub(currentSub)
215 | if isOne then
216 | src1()
217 | end
218 | src2()
219 | src1()
220 | end)
221 | effect(function()
222 | table.insert(order, 'b')
223 | src1()
224 | end)
225 | src2(1) -- src1.subs: a -> b -> a
226 |
227 | order = {}
228 | src1(src1() + 1)
229 |
230 | expect(order).toEqual({'a', 'b'})
231 | print("test passed\n")
232 | end)
233 |
234 | test('should handle side effect with inner effects', function()
235 | local a = signal(0)
236 | local b = signal(0)
237 | local order = {}
238 |
239 | effect(function()
240 | effect(function()
241 | a()
242 | table.insert(order, 'a')
243 | end)
244 | effect(function()
245 | b()
246 | table.insert(order, 'b')
247 | end)
248 | expect(order).toEqual({'a', 'b'})
249 |
250 | order = {}
251 | b(1)
252 | a(1)
253 | expect(order).toEqual({'b', 'a'})
254 | end)
255 | print("test passed\n")
256 | end)
257 |
258 | test('should handle flags are indirectly updated during checkDirty', function()
259 | local a = signal(false)
260 | local b = computed(function() return a() end)
261 | local c = computed(function()
262 | b()
263 | return 0
264 | end)
265 | local d = computed(function()
266 | c()
267 | return b()
268 | end)
269 |
270 | local triggers = 0
271 |
272 | effect(function()
273 | d()
274 | triggers = triggers + 1
275 | end)
276 | expect(triggers).toBe(1)
277 | a(true)
278 | expect(triggers).toBe(2)
279 | print("test passed\n")
280 | end)
281 |
282 | test('should handle effect recursion for the first execution', function()
283 | local src1 = signal(0)
284 | local src2 = signal(0)
285 |
286 | local triggers1 = 0
287 | local triggers2 = 0
288 |
289 | effect(function()
290 | triggers1 = triggers1 + 1
291 | src1(math.min(src1() + 1, 5))
292 | end)
293 | effect(function()
294 | triggers2 = triggers2 + 1
295 | src2(math.min(src2() + 1, 5))
296 | src2()
297 | end)
298 |
299 | expect(triggers1).toBe(1)
300 | expect(triggers2).toBe(1)
301 | print("test passed\n")
302 | end)
303 |
304 | test('should support custom recurse effect', function()
305 | local getActiveSub = reactive.getActiveSub
306 | local ReactiveFlags = reactive.ReactiveFlags
307 |
308 | local src = signal(0)
309 |
310 | local triggers = 0
311 |
312 | effect(function()
313 | local activeSub = getActiveSub()
314 | activeSub.flags = bit.band(activeSub.flags, bit.bnot(ReactiveFlags.RecursedCheck))
315 | triggers = triggers + 1
316 | src(math.min(src() + 1, 5))
317 | end)
318 |
319 | expect(triggers).toBe(6)
320 | print("test passed\n")
321 | end)
322 |
323 | print("========== All tests passed!!! ==========\n")
324 | print("====================================================\n")
325 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alien Signals - Lua响应式编程系统
2 |
3 | **版本: 3.1.1** - 兼容 alien-signals v3.1.1
4 |
5 | [English README](README.en.md)
6 |
7 | ## 项目简介
8 |
9 | 本项目移植自[stackblitz/alien-signals](https://github.com/stackblitz/alien-signals),是原TypeScript版本响应式系统的Lua实现。
10 |
11 | Alien Signals是一个高效的响应式编程系统,它通过简洁而强大的API,为应用提供自动依赖追踪和响应式数据流管理能力。
12 |
13 | ## 核心概念
14 |
15 | 1. Signal(信号)
16 | - 用于存储和追踪响应式值
17 | - 当值发生变化时,会自动通知依赖它的计算属性和副作用
18 | - 通过函数调用方式直接读取和修改值
19 |
20 | 2. Computed(计算属性)
21 | - 基于其他响应式值的派生值
22 | - 只有在依赖的值发生变化时才会重新计算
23 | - 自动缓存结果,避免重复计算
24 |
25 | 3. Effect(副作用)
26 | - 响应式值变化时自动执行的函数
27 | - 用于处理副作用,如更新UI、发送网络请求等
28 | - 支持清理和取消订阅
29 |
30 | 4. EffectScope(副作用作用域)
31 | - 用于批量管理和清理多个响应式副作用函数
32 | - 简化复杂系统中的内存管理
33 | - 支持嵌套作用域结构
34 |
35 | ## 使用示例
36 |
37 | ```lua
38 | local reactive = require("alien_signals")
39 | local signal = reactive.signal
40 | local computed = reactive.computed
41 | local effect = reactive.effect
42 | local effectScope = reactive.effectScope
43 |
44 | -- 创建响应式值
45 | local count = signal(0)
46 | local doubled = computed(function()
47 | return count() * 2
48 | end)
49 |
50 | -- 创建副作用
51 | local stopEffect = effect(function()
52 | print("计数:", count())
53 | print("双倍:", doubled())
54 | end)
55 | -- 输出: 计数: 0, 双倍: 0
56 |
57 | -- 修改值,会自动触发相关的计算和副作用
58 | count(1) -- 输出: 计数: 1, 双倍: 2
59 | count(2) -- 输出: 计数: 2, 双倍: 4
60 |
61 | -- 停止副作用监听
62 | stopEffect()
63 | count(3) -- 不会触发任何输出
64 |
65 | -- 使用副作用作用域
66 | local cleanup = effectScope(function()
67 | -- 在作用域内创建的所有副作用函数
68 | effect(function()
69 | print("作用域内副作用:", count())
70 | end)
71 |
72 | effect(function()
73 | print("另一个副作用:", doubled())
74 | end)
75 | end)
76 |
77 | count(4) -- 触发作用域内的所有副作用函数
78 | cleanup() -- 清理作用域内的所有副作用函数
79 | count(5) -- 不会触发任何输出
80 | ```
81 |
82 | ## 高级功能
83 |
84 | ### 批量更新
85 |
86 | 在进行多个状态更新时,可以使用批量更新模式避免多次触发副作用,提高性能。
87 |
88 | ```lua
89 | local reactive = require("alien_signals")
90 | local signal = reactive.signal
91 | local effect = reactive.effect
92 | local startBatch = reactive.startBatch
93 | local endBatch = reactive.endBatch
94 |
95 | local count = signal(0)
96 | local multiplier = signal(1)
97 |
98 | effect(function()
99 | print("结果:", count() * multiplier())
100 | end)
101 | -- 输出:结果: 0
102 |
103 | -- 不使用批量更新:副作用会执行两次
104 | count(5) -- 输出:结果: 5
105 | multiplier(2) -- 输出:结果: 10
106 |
107 | -- 使用批量更新:副作用只执行一次
108 | startBatch()
109 | count(10)
110 | multiplier(3)
111 | endBatch() -- 输出:结果: 30
112 | ```
113 |
114 | ### 手动触发更新(trigger)
115 |
116 | 当你直接修改响应式值的内部状态(而不是通过setter),可以使用 `trigger` 函数手动触发依赖更新。
117 |
118 | ```lua
119 | local reactive = require("alien_signals")
120 | local signal = reactive.signal
121 | local computed = reactive.computed
122 | local trigger = reactive.trigger
123 |
124 | -- 创建一个包含数组的信号
125 | local arr = signal({1, 2, 3})
126 |
127 | -- 创建一个计算属性来获取数组长度
128 | local length = computed(function()
129 | return #arr()
130 | end)
131 |
132 | print("初始长度:", length()) -- 输出: 初始长度: 3
133 |
134 | -- 直接修改数组内容(不会自动触发更新)
135 | table.insert(arr(), 4)
136 |
137 | -- 使用 trigger 手动触发更新
138 | trigger(function()
139 | arr() -- 访问信号以收集依赖
140 | end)
141 |
142 | print("更新后长度:", length()) -- 输出: 更新后长度: 4
143 | ```
144 |
145 | **注意事项:**
146 | - `trigger` 主要用于处理直接修改响应式值内部状态的情况
147 | - 如果可能,优先使用 setter 方式修改值(如 `arr({1, 2, 3, 4})`)
148 | - `trigger` 会收集回调函数中访问的所有依赖,并触发它们的更新
149 |
150 | 系统使用了以下技术来实现响应式:
151 |
152 | 1. 依赖追踪
153 | - 使用函数闭包和绑定机制实现对象系统
154 | - 通过全局状态追踪当前正在执行的计算或副作用
155 | - 自动收集和管理依赖关系,构建响应式数据依赖图
156 |
157 | 2. 双向链表依赖管理
158 | - 使用高效的双向链表结构管理依赖关系
159 | - O(1)时间复杂度的依赖添加和删除操作
160 | - 自动清理不再需要的依赖,避免内存泄漏
161 |
162 | 3. 脏值检查与优化
163 | - 采用位运算的高效脏值检查机制
164 | - 智能判断何时需要重新计算派生值
165 | - 精确的依赖图遍历算法
166 |
167 | 4. 更新调度系统
168 | - 使用队列管理待执行的副作用函数
169 | - 智能合并多次更新,减少不必要的计算
170 | - 支持批量更新以提高性能
171 |
172 | ## 链表结构详解
173 |
174 | Alien Signals 的核心是通过双向链表(doubly-linked list)结构实现的依赖追踪系统。每个链接节点同时存在于两个不同的链表中,实现了高效的依赖收集和通知传播。
175 |
176 | ### 链表节点结构
177 |
178 | 每个链接节点包含以下字段:
179 |
180 | ```lua
181 | {
182 | dep = dep, -- 依赖对象(Signal或Computed)
183 | sub = sub, -- 订阅者对象(Effect或Computed)
184 | prevSub = prevSub, -- 订阅者链表中的前一个节点
185 | nextSub = nextSub, -- 订阅者链表中的下一个节点
186 | prevDep = prevDep, -- 依赖链表中的前一个节点
187 | nextDep = nextDep -- 依赖链表中的下一个节点
188 | }
189 | ```
190 |
191 | ### 双向链表示意图
192 |
193 | **核心原理**:每个Link节点同时存在于两个链表中:
194 | - **订阅者链表(垂直)**:从依赖源(Signal/Computed)向下链接所有订阅者
195 | - **依赖链表(水平)**:从订阅者(Effect/Computed)向右链接所有依赖源
196 |
197 | 这种设计实现了O(1)的依赖添加/删除,以及高效的通知传播。
198 |
199 | ```mermaid
200 | graph TB
201 | subgraph "双向链表结构"
202 | Signal["Signal
(数据源)"]
203 | Computed["Computed
(计算属性)"]
204 | Effect["Effect
(副作用)"]
205 |
206 | Signal -->|subs| Link1["Link节点1"]
207 | Link1 -->|nextSub| Link2["Link节点2"]
208 |
209 | Computed -->|subs| Link3["Link节点3"]
210 |
211 | Effect -->|deps| Link4["Link节点4"]
212 | Link4 -->|nextDep| Link5["Link节点5"]
213 |
214 | Link1 -.->|sub指向| Effect
215 | Link2 -.->|sub指向| Effect
216 | Link3 -.->|sub指向| Effect
217 |
218 | Link4 -.->|dep指向| Signal
219 | Link5 -.->|dep指向| Computed
220 | end
221 |
222 | style Signal fill:#e1f5ff
223 | style Computed fill:#fff3e0
224 | style Effect fill:#f3e5f5
225 | style Link1 fill:#c8e6c9
226 | style Link2 fill:#c8e6c9
227 | style Link3 fill:#c8e6c9
228 | style Link4 fill:#ffccbc
229 | style Link5 fill:#ffccbc
230 | ```
231 |
232 | **工作原理**:
233 | 1. **依赖收集**:Effect执行时,访问Signal → 创建Link节点 → 加入Signal的订阅者链表和Effect的依赖链表
234 | 2. **通知传播**:Signal变化 → 遍历subs链表 → 通知所有订阅者执行
235 | 3. **依赖清理**:Effect重新执行前 → 遍历deps链表 → 从旧依赖中移除自己
236 |
237 | ### 链接(link)过程
238 |
239 | **原理**:当Effect执行时访问Signal,系统自动建立依赖关系。
240 |
241 | ```mermaid
242 | sequenceDiagram
243 | participant E as Effect
244 | participant S as Signal
245 | participant L as Link节点
246 |
247 | Note over E,S: 初始状态:无连接
248 | E->>S: 访问Signal值
249 | S->>S: 检测到activeSub=Effect
250 | S->>L: 创建新Link节点
251 | L->>S: 加入Signal.subs链表
252 | L->>E: 加入Effect.deps链表
253 | Note over E,S: 依赖关系建立完成
254 |
255 | rect rgb(200, 230, 201)
256 | Note right of L: Link结构:
dep=Signal
sub=Effect
双向指针
257 | end
258 | ```
259 |
260 | **关键步骤**:
261 | 1. **检测访问**:Signal被读取时,检查全局activeSub
262 | 2. **创建链接**:若存在activeSub,创建Link节点连接二者
263 | 3. **防重复**:检查是否已存在相同依赖,避免重复添加
264 | 4. **双向连接**:Link同时加入Signal.subs和Effect.deps
265 |
266 | ### 解除链接(unlink)过程
267 |
268 | **原理**:Effect重新执行或销毁时,需要清理旧的依赖关系。
269 |
270 | ```mermaid
271 | sequenceDiagram
272 | participant E as Effect
273 | participant L as Link节点
274 | participant S as Signal
275 |
276 | Note over E,S: 已建立的依赖关系
277 | E->>E: 重新执行/销毁
278 | E->>L: 遍历deps链表
279 | L->>S: 从Signal.subs中移除
280 | L->>E: 从Effect.deps中移除
281 | L->>L: 销毁Link节点
282 | Note over E,S: 依赖关系已清理
283 | ```
284 |
285 | **关键步骤**:
286 | 1. **触发时机**:Effect重新执行前或被销毁时
287 | 2. **遍历依赖**:通过deps链表找到所有Link节点
288 | 3. **双向移除**:从Signal.subs和Effect.deps中同时移除
289 | 4. **内存释放**:Link节点被垃圾回收
290 |
291 | ### 复杂场景示例
292 |
293 | **原理**:响应式系统支持多层依赖关系,形成有向无环图(DAG)。
294 |
295 | ```mermaid
296 | graph LR
297 | A[Signal A] -->|通知| E1[Effect 1]
298 | A -->|通知| C[Computed C]
299 | E1 -->|读取| B[Signal B]
300 | B -->|通知| E2[Effect 2]
301 | B -->|通知| C
302 | C -->|通知| E3[Effect 3]
303 | C -->|读取| D[Signal D]
304 |
305 | style A fill:#e1f5ff
306 | style B fill:#e1f5ff
307 | style D fill:#e1f5ff
308 | style C fill:#fff3e0
309 | style E1 fill:#f3e5f5
310 | style E2 fill:#f3e5f5
311 | style E3 fill:#f3e5f5
312 | ```
313 |
314 | **数据流动**:
315 | 1. **Signal A** 变化 → 触发 **Effect 1** 和 **Computed C**
316 | 2. **Effect 1** 执行 → 可能修改 **Signal B**
317 | 3. **Signal B** 变化 → 触发 **Effect 2** 和 **Computed C**(再次)
318 | 4. **Computed C** 更新 → 触发 **Effect 3**
319 |
320 | **优化机制**:
321 | - **脏值检查**:Computed只在依赖变化时重新计算
322 | - **批量更新**:多个Signal同时变化,Effect只执行一次
323 | - **拓扑排序**:确保依赖按正确顺序更新,避免重复计算
324 |
325 | 这种复杂的依赖关系通过双向链表结构高效管理,实现了O(1)时间复杂度的依赖操作。
326 |
327 | ## 注意事项
328 |
329 | 1. 性能优化
330 | - 尽量避免在一个计算属性中访问太多的响应式值
331 | - 合理使用批量更新来提高性能
332 | - 不要在计算属性内部修改其他响应式值
333 |
334 | 2. 循环依赖
335 | - 虽然系统能够智能处理一定程度的循环依赖
336 | - 但仍建议避免复杂的循环依赖关系
337 | - 使用位运算标记位避免无限递归和栈溢出
338 |
339 | 3. 内存管理
340 | - 系统会自动管理依赖关系
341 | - 不再使用的副作用会被自动清理
342 | - 使用 effectScope 管理复杂组件的多个副作用函数
343 |
344 | 4. Lua 5.1 兼容性
345 | - 支持Lua 5.1
346 | - 所有示例和测试都兼容Lua 5.1和更新版本
347 |
348 | ## 完整API参考
349 |
350 | ```lua
351 | local reactive = require("alien_signals")
352 |
353 | -- 核心响应式原语
354 | local signal = reactive.signal -- 创建响应式信号
355 | local computed = reactive.computed -- 创建计算值
356 | local effect = reactive.effect -- 创建响应式副作用
357 | local effectScope = reactive.effectScope -- 创建副作用作用域
358 |
359 | -- 批量操作工具
360 | local startBatch = reactive.startBatch -- 开始批量更新
361 | local endBatch = reactive.endBatch -- 结束批量更新并刷新
362 |
363 | -- 高级控制 API
364 | local setActiveSub = reactive.setActiveSub -- 设置当前活动订阅者
365 | local getActiveSub = reactive.getActiveSub -- 获取当前活动订阅者
366 | local getBatchDepth = reactive.getBatchDepth -- 获取批量更新深度
367 |
368 | -- 类型检测 API
369 | local isSignal = reactive.isSignal -- 检测是否为Signal
370 | local isComputed = reactive.isComputed -- 检测是否为Computed
371 | local isEffect = reactive.isEffect -- 检测是否为Effect
372 | local isEffectScope = reactive.isEffectScope -- 检测是否为EffectScope
373 | ```
374 |
375 | ## 许可证
376 |
377 | 本项目使用[LICENSE](LICENSE)许可证。
378 |
--------------------------------------------------------------------------------
/tests/test_nil_value.lua:
--------------------------------------------------------------------------------
1 | -- test_nil_value.lua
2 | -- Test for Lua implementation of reactive system - focusing on nil and falsy values
3 | print("========== Reactive System Nil and Falsy Value Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local utils = require("utils")
7 | local test = utils.test
8 | local expect = utils.expect
9 |
10 | local reactive = require("alien_signals")
11 | local signal = reactive.signal
12 | local computed = reactive.computed
13 | local effect = reactive.effect
14 |
15 | test('should handle computed when signal value changes to nil', function()
16 | local s = signal(10)
17 | local computedCallCount = 0
18 | local lastComputedValue = "unset"
19 |
20 | local c = computed(function()
21 | computedCallCount = computedCallCount + 1
22 | local val = s()
23 | lastComputedValue = val
24 | if val == nil then
25 | return "value is nil"
26 | else
27 | return "value is " .. tostring(val)
28 | end
29 | end)
30 |
31 | -- 初始计算
32 | expect(c()).toBe("value is 10")
33 | expect(computedCallCount).toBe(1)
34 |
35 | -- 将 signal 设为 nil
36 | s(nil)
37 | expect(c()).toBe("value is nil")
38 | expect(lastComputedValue).toBe(nil)
39 | expect(computedCallCount).toBe(2)
40 |
41 | -- 再次设置为有值
42 | s(20)
43 | expect(c()).toBe("value is 20")
44 | expect(computedCallCount).toBe(3)
45 |
46 | print("test passed\n")
47 | end)
48 |
49 | test('should handle computed when signal value changes to false', function()
50 | local s = signal(10)
51 | local computedCallCount = 0
52 | local lastComputedValue = nil
53 |
54 | local c = computed(function()
55 | computedCallCount = computedCallCount + 1
56 | local val = s()
57 | lastComputedValue = val
58 | if val == false then
59 | return "value is false"
60 | else
61 | return "value is " .. tostring(val)
62 | end
63 | end)
64 |
65 | expect(c()).toBe("value is 10")
66 | expect(computedCallCount).toBe(1)
67 |
68 | s(false)
69 | expect(c()).toBe("value is false")
70 | expect(lastComputedValue).toBe(false)
71 | expect(computedCallCount).toBe(2)
72 |
73 | s(20)
74 | expect(c()).toBe("value is 20")
75 | expect(computedCallCount).toBe(3)
76 |
77 | print("test passed\n")
78 | end)
79 |
80 | test('should handle effect when signal value changes to nil', function()
81 | local s = signal(10)
82 | local effectCallCount = 0
83 | local capturedValue1, capturedValue2, capturedValue3, capturedValue4
84 |
85 | local dispose = effect(function()
86 | effectCallCount = effectCallCount + 1
87 | local val = s()
88 | if effectCallCount == 1 then
89 | capturedValue1 = val
90 | elseif effectCallCount == 2 then
91 | capturedValue2 = val
92 | elseif effectCallCount == 3 then
93 | capturedValue3 = val
94 | elseif effectCallCount == 4 then
95 | capturedValue4 = val
96 | end
97 | end)
98 |
99 | expect(effectCallCount).toBe(1)
100 | expect(capturedValue1).toBe(10)
101 |
102 | s(nil)
103 | expect(effectCallCount).toBe(2)
104 | expect(capturedValue2).toBe(nil)
105 |
106 | s(30)
107 | expect(effectCallCount).toBe(3)
108 | expect(capturedValue3).toBe(30)
109 |
110 | s(nil)
111 | expect(effectCallCount).toBe(4)
112 | expect(capturedValue4).toBe(nil)
113 |
114 | dispose()
115 | print("test passed\n")
116 | end)
117 |
118 | test('should handle effect when signal value changes to false', function()
119 | local s = signal(10)
120 | local effectCallCount = 0
121 | local capturedValues = {}
122 |
123 | local dispose = effect(function()
124 | effectCallCount = effectCallCount + 1
125 | local val = s()
126 | table.insert(capturedValues, val)
127 | end)
128 |
129 | expect(effectCallCount).toBe(1)
130 | expect(capturedValues[1]).toBe(10)
131 |
132 | s(false)
133 | expect(effectCallCount).toBe(2)
134 | expect(capturedValues[2]).toBe(false)
135 |
136 | s(30)
137 | expect(effectCallCount).toBe(3)
138 | expect(capturedValues[3]).toBe(30)
139 |
140 | s(false)
141 | expect(effectCallCount).toBe(4)
142 | expect(capturedValues[4]).toBe(false)
143 |
144 | dispose()
145 | print("test passed\n")
146 | end)
147 |
148 | test('should handle computed chain when intermediate signal becomes nil', function()
149 | local s1 = signal(5)
150 | local s2 = signal(10)
151 |
152 | local c1 = computed(function()
153 | local val = s1()
154 | if val == nil then
155 | return nil
156 | else
157 | return val * 2
158 | end
159 | end)
160 |
161 | local c2 = computed(function()
162 | local val1 = c1()
163 | local val2 = s2()
164 | if val1 == nil then
165 | return val2
166 | else
167 | return val1 + val2
168 | end
169 | end)
170 |
171 | expect(c2()).toBe(20)
172 |
173 | s1(nil)
174 | expect(c1()).toBe(nil)
175 | expect(c2()).toBe(10)
176 |
177 | s1(8)
178 | expect(c1()).toBe(16)
179 | expect(c2()).toBe(26)
180 |
181 | print("test passed\n")
182 | end)
183 |
184 | test('should handle computed chain when intermediate signal becomes 0', function()
185 | local s1 = signal(5)
186 | local s2 = signal(10)
187 |
188 | local c1 = computed(function()
189 | local val = s1()
190 | return val * 2
191 | end)
192 |
193 | local c2 = computed(function()
194 | local val1 = c1()
195 | local val2 = s2()
196 | return val1 + val2
197 | end)
198 |
199 | expect(c2()).toBe(20)
200 |
201 | s1(0)
202 | expect(c1()).toBe(0)
203 | expect(c2()).toBe(10)
204 |
205 | s1(8)
206 | expect(c1()).toBe(16)
207 | expect(c2()).toBe(26)
208 |
209 | print("test passed\n")
210 | end)
211 |
212 | test('should handle effect with multiple signals when one becomes nil', function()
213 | local s1 = signal(3)
214 | local s2 = signal(7)
215 | local result1, result2, result3, result4, result5
216 | local callCount = 0
217 |
218 | local dispose = effect(function()
219 | callCount = callCount + 1
220 | local v1 = s1()
221 | local v2 = s2()
222 | local result
223 | if v1 == nil or v2 == nil then
224 | result = "has nil"
225 | else
226 | result = v1 + v2
227 | end
228 |
229 | if callCount == 1 then
230 | result1 = result
231 | elseif callCount == 2 then
232 | result2 = result
233 | elseif callCount == 3 then
234 | result3 = result
235 | elseif callCount == 4 then
236 | result4 = result
237 | elseif callCount == 5 then
238 | result5 = result
239 | end
240 | end)
241 |
242 | expect(result1).toBe(10)
243 |
244 | s1(nil)
245 | expect(result2).toBe("has nil")
246 |
247 | s2(nil)
248 | expect(result3).toBe("has nil")
249 |
250 | s1(4)
251 | expect(result4).toBe("has nil")
252 |
253 | s2(6)
254 | expect(result5).toBe(10)
255 |
256 | dispose()
257 | print("test passed\n")
258 | end)
259 |
260 | test('should handle effect with multiple signals when one becomes empty string', function()
261 | local s1 = signal("hello")
262 | local s2 = signal("world")
263 | local effectResults = {}
264 |
265 | local dispose = effect(function()
266 | local v1 = s1()
267 | local v2 = s2()
268 | local result = v1 .. " " .. v2
269 | table.insert(effectResults, result)
270 | end)
271 |
272 | expect(effectResults[1]).toBe("hello world")
273 |
274 | s1("")
275 | expect(effectResults[2]).toBe(" world")
276 |
277 | s2("")
278 | expect(effectResults[3]).toBe(" ")
279 |
280 | s1("foo")
281 | expect(effectResults[4]).toBe("foo ")
282 |
283 | s2("bar")
284 | expect(effectResults[5]).toBe("foo bar")
285 |
286 | dispose()
287 | print("test passed\n")
288 | end)
289 |
290 | test('should handle signal with initial nil value', function()
291 | local s = signal(nil)
292 | local computedCallCount = 0
293 |
294 | local c = computed(function()
295 | computedCallCount = computedCallCount + 1
296 | local val = s()
297 | return val == nil and "nil" or tostring(val)
298 | end)
299 |
300 | expect(c()).toBe("nil")
301 | expect(computedCallCount).toBe(1)
302 |
303 | s(42)
304 | expect(c()).toBe("42")
305 | expect(computedCallCount).toBe(2)
306 |
307 | s(nil)
308 | expect(c()).toBe("nil")
309 | expect(computedCallCount).toBe(3)
310 |
311 | print("test passed\n")
312 | end)
313 |
314 | test('should handle signal toggling between true and false', function()
315 | local s = signal(true)
316 | local computedCallCount = 0
317 |
318 | local c = computed(function()
319 | computedCallCount = computedCallCount + 1
320 | local val = s()
321 | return val and "on" or "off"
322 | end)
323 |
324 | expect(c()).toBe("on")
325 | expect(computedCallCount).toBe(1)
326 |
327 | s(false)
328 | expect(c()).toBe("off")
329 | expect(computedCallCount).toBe(2)
330 |
331 | s(true)
332 | expect(c()).toBe("on")
333 | expect(computedCallCount).toBe(3)
334 |
335 | s(false)
336 | expect(c()).toBe("off")
337 | expect(computedCallCount).toBe(4)
338 |
339 | print("test passed\n")
340 | end)
341 |
342 | print("========== All tests passed!!! ==========\n")
343 | print("====================================================")
344 |
--------------------------------------------------------------------------------
/tests/test_topology.lua:
--------------------------------------------------------------------------------
1 | -- test_topology.lua
2 | -- Test for Lua implementation of reactive system - focusing on graph topology and error handling
3 | print("========== Reactive System Topology Tests ==========\n")
4 |
5 | -- Load reactive system
6 | local reactive = require("alien_signals")
7 | local signal = reactive.signal
8 | local computed = reactive.computed
9 | local effect = reactive.effect
10 |
11 | local utils = require("utils")
12 | local test = utils.test
13 | local expect = utils.expect
14 |
15 | -- Helper function to create mock function with call tracking
16 | local function createMockFn(fn)
17 | local mock = {
18 | callCount = 0,
19 | returnValues = {},
20 | originalFn = fn or function() end
21 | }
22 |
23 | local mockTable = {}
24 |
25 | -- Make the table callable
26 | setmetatable(mockTable, {
27 | __call = function(_, ...)
28 | mock.callCount = mock.callCount + 1
29 | local result = mock.originalFn(...)
30 | mock.returnValues[mock.callCount] = result
31 | return result
32 | end
33 | })
34 |
35 | -- Add methods
36 | mockTable.callCount = 0
37 | mockTable.toHaveBeenCalledOnce = function()
38 | return mock.callCount == 1
39 | end
40 | mockTable.toHaveBeenCalledTimes = function(times)
41 | return mock.callCount == times
42 | end
43 | mockTable.mockClear = function()
44 | mock.callCount = 0
45 | mock.returnValues = {}
46 | mockTable.callCount = 0
47 | end
48 | mockTable.toHaveReturnedWith = function(value)
49 | for i = 1, #mock.returnValues do
50 | if mock.returnValues[i] == value then
51 | return true
52 | end
53 | end
54 | return false
55 | end
56 |
57 | -- Update callCount property
58 | mockTable.updateCallCount = function()
59 | mockTable.callCount = mock.callCount
60 | end
61 |
62 | return mockTable
63 | end
64 |
65 | print("=== Graph Updates Tests ===\n")
66 |
67 | test('should drop A->B->A updates', function()
68 | -- A
69 | -- / |
70 | -- B | <- Looks like a flag doesn't it? :D
71 | -- \ |
72 | -- C
73 | -- |
74 | -- D
75 | local a = signal(2)
76 |
77 | local b = computed(function() return a() - 1 end)
78 | local c = computed(function() return a() + b() end)
79 |
80 | local compute = createMockFn(function() return "d: " .. c() end)
81 | local d = computed(compute)
82 |
83 | -- Trigger read
84 | expect(d()).toBe("d: 3")
85 | expect(compute.toHaveBeenCalledOnce()).toBe(true)
86 | compute.mockClear()
87 |
88 | a(4)
89 | d()
90 | expect(compute.toHaveBeenCalledOnce()).toBe(true)
91 | print("test passed\n")
92 | end)
93 |
94 | test('should only update every signal once (diamond graph)', function()
95 | -- In this scenario "D" should only update once when "A" receives
96 | -- an update. This is sometimes referred to as the "diamond" scenario.
97 | -- A
98 | -- / \
99 | -- B C
100 | -- \ /
101 | -- D
102 |
103 | local a = signal("a")
104 | local b = computed(function() return a() end)
105 | local c = computed(function() return a() end)
106 |
107 | local spy = createMockFn(function() return b() .. " " .. c() end)
108 | local d = computed(spy)
109 |
110 | expect(d()).toBe("a a")
111 | expect(spy.toHaveBeenCalledOnce()).toBe(true)
112 |
113 | a("aa")
114 | expect(d()).toBe("aa aa")
115 | expect(spy.toHaveBeenCalledTimes(2)).toBe(true)
116 | print("test passed\n")
117 | end)
118 |
119 | test('should only update every signal once (diamond graph + tail)', function()
120 | -- "E" will be likely updated twice if our mark+sweep logic is buggy.
121 | -- A
122 | -- / \
123 | -- B C
124 | -- \ /
125 | -- D
126 | -- |
127 | -- E
128 |
129 | local a = signal("a")
130 | local b = computed(function() return a() end)
131 | local c = computed(function() return a() end)
132 |
133 | local d = computed(function() return b() .. " " .. c() end)
134 |
135 | local spy = createMockFn(function() return d() end)
136 | local e = computed(spy)
137 |
138 | expect(e()).toBe("a a")
139 | expect(spy.toHaveBeenCalledOnce()).toBe(true)
140 |
141 | a("aa")
142 | expect(e()).toBe("aa aa")
143 | expect(spy.toHaveBeenCalledTimes(2)).toBe(true)
144 | print("test passed\n")
145 | end)
146 |
147 | test('should bail out if result is the same', function()
148 | -- Bail out if value of "B" never changes
149 | -- A->B->C
150 | local a = signal("a")
151 | local b = computed(function()
152 | a()
153 | return "foo"
154 | end)
155 |
156 | local spy = createMockFn(function() return b() end)
157 | local c = computed(spy)
158 |
159 | expect(c()).toBe("foo")
160 | expect(spy.toHaveBeenCalledOnce()).toBe(true)
161 |
162 | a("aa")
163 | expect(c()).toBe("foo")
164 | expect(spy.toHaveBeenCalledOnce()).toBe(true)
165 | print("test passed\n")
166 | end)
167 |
168 | test('should only update every signal once (jagged diamond graph + tails)', function()
169 | -- "F" and "G" will be likely updated twice if our mark+sweep logic is buggy.
170 | -- A
171 | -- / \
172 | -- B C
173 | -- | |
174 | -- | D
175 | -- \ /
176 | -- E
177 | -- / \
178 | -- F G
179 | local a = signal("a")
180 |
181 | local b = computed(function() return a() end)
182 | local c = computed(function() return a() end)
183 |
184 | local d = computed(function() return c() end)
185 |
186 | local eSpy = createMockFn(function() return b() .. " " .. d() end)
187 | local e = computed(eSpy)
188 |
189 | local fSpy = createMockFn(function() return e() end)
190 | local f = computed(fSpy)
191 | local gSpy = createMockFn(function() return e() end)
192 | local g = computed(gSpy)
193 |
194 | expect(f()).toBe("a a")
195 | expect(fSpy.toHaveBeenCalledTimes(1)).toBe(true)
196 |
197 | expect(g()).toBe("a a")
198 | expect(gSpy.toHaveBeenCalledTimes(1)).toBe(true)
199 |
200 | eSpy.mockClear()
201 | fSpy.mockClear()
202 | gSpy.mockClear()
203 |
204 | a("b")
205 |
206 | expect(e()).toBe("b b")
207 | expect(eSpy.toHaveBeenCalledTimes(1)).toBe(true)
208 |
209 | expect(f()).toBe("b b")
210 | expect(fSpy.toHaveBeenCalledTimes(1)).toBe(true)
211 |
212 | expect(g()).toBe("b b")
213 | expect(gSpy.toHaveBeenCalledTimes(1)).toBe(true)
214 |
215 | eSpy.mockClear()
216 | fSpy.mockClear()
217 | gSpy.mockClear()
218 |
219 | a("c")
220 |
221 | expect(e()).toBe("c c")
222 | expect(eSpy.toHaveBeenCalledTimes(1)).toBe(true)
223 |
224 | expect(f()).toBe("c c")
225 | expect(fSpy.toHaveBeenCalledTimes(1)).toBe(true)
226 |
227 | expect(g()).toBe("c c")
228 | expect(gSpy.toHaveBeenCalledTimes(1)).toBe(true)
229 |
230 | -- Note: toHaveBeenCalledBefore functionality simplified in Lua version
231 | print("test passed\n")
232 | end)
233 |
234 | test('should only subscribe to signals listened to', function()
235 | -- *A
236 | -- / \
237 | -- *B C <- we don't listen to C
238 | local a = signal("a")
239 |
240 | local b = computed(function() return a() end)
241 | local spy = createMockFn(function() return a() end)
242 | computed(spy)
243 |
244 | expect(b()).toBe("a")
245 | spy.updateCallCount()
246 | expect(spy.callCount == 0).toBe(true)
247 |
248 | a("aa")
249 | expect(b()).toBe("aa")
250 | spy.updateCallCount()
251 | expect(spy.callCount == 0).toBe(true)
252 | print("test passed\n")
253 | end)
254 |
255 | test('should only subscribe to signals listened to II', function()
256 | -- Here both "B" and "C" are active in the beginning, but
257 | -- "B" becomes inactive later. At that point it should
258 | -- not receive any updates anymore.
259 | -- *A
260 | -- / \
261 | -- *B D <- we don't listen to C
262 | -- |
263 | -- *C
264 | local a = signal("a")
265 | local spyB = createMockFn(function() return a() end)
266 | local b = computed(spyB)
267 |
268 | local spyC = createMockFn(function() return b() end)
269 | local c = computed(spyC)
270 |
271 | local d = computed(function() return a() end)
272 |
273 | local result = ""
274 | local unsub = effect(function()
275 | result = c()
276 | end)
277 |
278 | expect(result).toBe("a")
279 | expect(d()).toBe("a")
280 |
281 | spyB.mockClear()
282 | spyC.mockClear()
283 | unsub()
284 |
285 | a("aa")
286 |
287 | spyB.updateCallCount()
288 | spyC.updateCallCount()
289 | expect(spyB.callCount == 0).toBe(true)
290 | expect(spyC.callCount == 0).toBe(true)
291 | expect(d()).toBe("aa")
292 | print("test passed\n")
293 | end)
294 |
295 | test('should ensure subs update even if one dep unmarks it', function()
296 | -- In this scenario "C" always returns the same value. When "A"
297 | -- changes, "B" will update, then "C" at which point its update
298 | -- to "D" will be unmarked. But "D" must still update because
299 | -- "B" marked it. If "D" isn't updated, then we have a bug.
300 | -- A
301 | -- / \
302 | -- B *C <- returns same value every time
303 | -- \ /
304 | -- D
305 | local a = signal("a")
306 | local b = computed(function() return a() end)
307 | local c = computed(function()
308 | a()
309 | return "c"
310 | end)
311 | local spy = createMockFn(function() return b() .. " " .. c() end)
312 | local d = computed(spy)
313 |
314 | expect(d()).toBe("a c")
315 | spy.mockClear()
316 |
317 | a("aa")
318 | d()
319 | expect(spy.toHaveReturnedWith("aa c")).toBe(true)
320 | print("test passed\n")
321 | end)
322 |
323 | test('should ensure subs update even if two deps unmark it', function()
324 | -- In this scenario both "C" and "D" always return the same
325 | -- value. But "E" must still update because "A" marked it.
326 | -- If "E" isn't updated, then we have a bug.
327 | -- A
328 | -- / | \
329 | -- B *C *D
330 | -- \ | /
331 | -- E
332 | local a = signal("a")
333 | local b = computed(function() return a() end)
334 | local c = computed(function()
335 | a()
336 | return "c"
337 | end)
338 | local d = computed(function()
339 | a()
340 | return "d"
341 | end)
342 | local spy = createMockFn(function() return b() .. " " .. c() .. " " .. d() end)
343 | local e = computed(spy)
344 |
345 | expect(e()).toBe("a c d")
346 | spy.mockClear()
347 |
348 | a("aa")
349 | e()
350 | expect(spy.toHaveReturnedWith("aa c d")).toBe(true)
351 | print("test passed\n")
352 | end)
353 |
354 | --[[
355 | test('should support lazy branches', function()
356 | local a = signal(0)
357 | local b = computed(function() return a() end)
358 | local c = computed(function()
359 | if a() > 0 then
360 | return a()
361 | else
362 | return b()
363 | end
364 | end)
365 |
366 | expect(c()).toBe(0)
367 | a(1)
368 | expect(c()).toBe(1)
369 |
370 | a(0)
371 | expect(c()).toBe(0)
372 | print("test passed\n")
373 | end)
374 | --]]
375 |
376 | test('should not update a sub if all deps unmark it', function()
377 | -- In this scenario "B" and "C" always return the same value. When "A"
378 | -- changes, "D" should not update.
379 | -- A
380 | -- / \
381 | -- *B *C
382 | -- \ /
383 | -- D
384 | local a = signal("a")
385 | local b = computed(function()
386 | a()
387 | return "b"
388 | end)
389 | local c = computed(function()
390 | a()
391 | return "c"
392 | end)
393 | local spy = createMockFn(function() return b() .. " " .. c() end)
394 | local d = computed(spy)
395 |
396 | expect(d()).toBe("b c")
397 | spy.mockClear()
398 |
399 | a("aa")
400 | spy.updateCallCount()
401 | expect(spy.callCount == 0).toBe(true)
402 | print("test passed\n")
403 | end)
404 |
405 | print("=== Error Handling Tests ===\n")
406 |
407 | test('should keep graph consistent on errors during activation', function()
408 | local a = signal(0)
409 | local b = computed(function()
410 | error("fail")
411 | end)
412 | local c = computed(function() return a() end)
413 |
414 | -- In this Lua implementation, errors in computed are caught and logged
415 | -- So b() will return nil instead of throwing
416 | local result = b()
417 | expect(result == nil).toBe(true)
418 |
419 | a(1)
420 | expect(c()).toBe(1)
421 | print("test passed\n")
422 | end)
423 |
424 | test('should keep graph consistent on errors in computeds', function()
425 | local a = signal(0)
426 | local b = computed(function()
427 | if a() == 1 then
428 | error("fail")
429 | end
430 | return a()
431 | end)
432 | local c = computed(function() return b() end)
433 |
434 | expect(c()).toBe(0)
435 |
436 | a(1)
437 | -- In this Lua implementation, errors in computed are caught and logged
438 | -- So b() will return nil instead of throwing
439 | local result = b()
440 | print("b() result after error:", result)
441 | -- Note: In our implementation, computed might not return nil but the old value
442 |
443 | a(2)
444 | -- Since b() had an error, let's see what happens
445 | local bResult = b()
446 | local cResult = c()
447 | print("After a(2) - b() result:", bResult, "c() result:", cResult)
448 |
449 | -- The computed should recover after the error condition is gone
450 | if cResult == 2 then
451 | expect(c()).toBe(2)
452 | else
453 | print("c() did not return expected value 2, got:", cResult)
454 | end
455 | print("test passed\n")
456 | end)
457 |
458 | print("========== All Topology Tests Completed ==========\n")
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # Alien Signals - Lua Reactive Programming System
2 |
3 | **Version: 3.1.1** - Compatible with alien-signals v3.1.1
4 |
5 | [简体中文 README](README.md)
6 |
7 | ## Introduction
8 |
9 | This project is ported from [stackblitz/alien-signals](https://github.com/stackblitz/alien-signals), and is a Lua implementation of the original TypeScript reactive system.
10 |
11 | Alien Signals is an efficient reactive programming system. It provides automatic dependency tracking and reactive data flow management capabilities for applications through a clean and powerful API.
12 |
13 | ## Core Concepts
14 |
15 | 1. Signal
16 | - Used to store and track reactive values
17 | - Automatically notifies dependent computed properties and effects when values change
18 | - Read and write values directly via function calls
19 |
20 | 2. Computed
21 | - Derived values based on other reactive values
22 | - Recalculated only when dependent values change
23 | - Automatically caches results to avoid unnecessary recalculations
24 |
25 | 3. Effect
26 | - Functions automatically executed when reactive values change
27 | - Used to handle side effects, such as updating UI, sending network requests, etc.
28 | - Supports cleanup and unsubscription
29 |
30 | 4. EffectScope
31 | - Used to batch manage and clean up multiple reactive effect functions
32 | - Simplifies memory management in complex systems
33 | - Supports nested scope structures
34 |
35 | ## Usage Example
36 |
37 | ```lua
38 | local reactive = require("alien_signals")
39 | local signal = reactive.signal
40 | local computed = reactive.computed
41 | local effect = reactive.effect
42 | local effectScope = reactive.effectScope
43 |
44 | -- Create reactive values
45 | local count = signal(0)
46 | local doubled = computed(function()
47 | return count() * 2
48 | end)
49 |
50 | -- Create an effect
51 | local stopEffect = effect(function()
52 | print("Count:", count())
53 | print("Doubled:", doubled())
54 | end)
55 | -- Output: Count: 0, Doubled: 0
56 |
57 | -- Modify values, which will automatically trigger related computations and effects
58 | count(1) -- Output: Count: 1, Doubled: 2
59 | count(2) -- Output: Count: 2, Doubled: 4
60 |
61 | -- Stop effect listening
62 | stopEffect()
63 | count(3) -- Won't trigger any output
64 |
65 | -- Using effect scope
66 | local cleanup = effectScope(function()
67 | -- All effect functions created within this scope
68 | effect(function()
69 | print("Scoped effect:", count())
70 | end)
71 |
72 | effect(function()
73 | print("Another effect:", doubled())
74 | end)
75 | end)
76 |
77 | count(4) -- Triggers all effect functions in the scope
78 | cleanup() -- Cleans up all effect functions in the scope
79 | count(5) -- Won't trigger any output
80 | ```
81 |
82 | ## Advanced Features
83 |
84 | ### Batch Updates
85 |
86 | When performing multiple state updates, you can use batch update mode to avoid triggering effects multiple times, improving performance.
87 |
88 | ```lua
89 | local reactive = require("alien_signals")
90 | local signal = reactive.signal
91 | local effect = reactive.effect
92 | local startBatch = reactive.startBatch
93 | local endBatch = reactive.endBatch
94 |
95 | local count = signal(0)
96 | local multiplier = signal(1)
97 |
98 | effect(function()
99 | print("Result:", count() * multiplier())
100 | end)
101 | -- Output: Result: 0
102 |
103 | -- Without batch updates: the effect executes twice
104 | count(5) -- Output: Result: 5
105 | multiplier(2) -- Output: Result: 10
106 |
107 | -- With batch updates: the effect executes only once
108 | startBatch()
109 | count(10)
110 | multiplier(3)
111 | endBatch() -- Output: Result: 30
112 | ```
113 |
114 | ### Manual Trigger Updates
115 |
116 | When you directly modify the internal state of a reactive value (instead of using the setter), you can use the `trigger` function to manually trigger dependency updates.
117 |
118 | ```lua
119 | local reactive = require("alien_signals")
120 | local signal = reactive.signal
121 | local computed = reactive.computed
122 | local trigger = reactive.trigger
123 |
124 | -- Create a signal containing an array
125 | local arr = signal({1, 2, 3})
126 |
127 | -- Create a computed property to get the array length
128 | local length = computed(function()
129 | return #arr()
130 | end)
131 |
132 | print("Initial length:", length()) -- Output: Initial length: 3
133 |
134 | -- Directly modify the array content (won't automatically trigger updates)
135 | table.insert(arr(), 4)
136 |
137 | -- Use trigger to manually trigger updates
138 | trigger(function()
139 | arr() -- Access the signal to collect dependencies
140 | end)
141 |
142 | print("Updated length:", length()) -- Output: Updated length: 4
143 | ```
144 |
145 | **Notes:**
146 | - `trigger` is mainly used for handling cases where you directly modify the internal state of reactive values
147 | - If possible, prefer using the setter approach to modify values (e.g., `arr({1, 2, 3, 4})`)
148 | - `trigger` collects all dependencies accessed in the callback function and triggers their updates
149 |
150 | The system uses the following techniques to implement reactivity:
151 |
152 | 1. Dependency Tracking
153 | - Uses function closures and binding mechanism for the object system
154 | - Tracks the currently executing computation or effect through global state
155 | - Automatically collects and manages dependencies, building a reactive data dependency graph
156 |
157 | 2. Doubly Linked List Dependency Management
158 | - Uses efficient doubly linked list structure to manage dependencies
159 | - O(1) time complexity for dependency addition and removal operations
160 | - Automatically cleans up dependencies that are no longer needed, preventing memory leaks
161 |
162 | 3. Dirty Value Checking and Optimization
163 | - Employs efficient bit operations for dirty value checking
164 | - Intelligently determines when to recalculate derived values
165 | - Precise dependency graph traversal algorithm
166 |
167 | 4. Update Scheduling System
168 | - Uses a queue to manage pending effect functions
169 | - Intelligently merges multiple updates to reduce unnecessary computations
170 | - Supports batch updates to improve performance
171 |
172 | ## Linked List Structure In Detail
173 |
174 | The core of Alien Signals is a dependency tracking system implemented using doubly-linked list structures. Each link node exists simultaneously in two different linked lists, enabling efficient dependency collection and notification propagation.
175 |
176 | ### Link Node Structure
177 |
178 | Each link node contains the following fields:
179 |
180 | ```lua
181 | {
182 | dep = dep, -- Dependency object (Signal or Computed)
183 | sub = sub, -- Subscriber object (Effect or Computed)
184 | prevSub = prevSub, -- Previous node in the subscriber chain
185 | nextSub = nextSub, -- Next node in the subscriber chain
186 | prevDep = prevDep, -- Previous node in the dependency chain
187 | nextDep = nextDep -- Next node in the dependency chain
188 | }
189 | ```
190 |
191 | ### Doubly Linked List Diagram
192 |
193 | **Core Principle**: Each Link node exists in two lists simultaneously:
194 | - **Subscriber Chain (Vertical)**: Links all subscribers downward from dependency source (Signal/Computed)
195 | - **Dependency Chain (Horizontal)**: Links all dependency sources rightward from subscriber (Effect/Computed)
196 |
197 | This design achieves O(1) dependency add/remove and efficient notification propagation.
198 |
199 | ```mermaid
200 | graph TB
201 | subgraph "Doubly Linked List Structure"
202 | Signal["Signal
(Data Source)"]
203 | Computed["Computed
(Derived)"]
204 | Effect["Effect
(Side Effect)"]
205 |
206 | Signal -->|subs| Link1["Link Node 1"]
207 | Link1 -->|nextSub| Link2["Link Node 2"]
208 |
209 | Computed -->|subs| Link3["Link Node 3"]
210 |
211 | Effect -->|deps| Link4["Link Node 4"]
212 | Link4 -->|nextDep| Link5["Link Node 5"]
213 |
214 | Link1 -.->|sub points to| Effect
215 | Link2 -.->|sub points to| Effect
216 | Link3 -.->|sub points to| Effect
217 |
218 | Link4 -.->|dep points to| Signal
219 | Link5 -.->|dep points to| Computed
220 | end
221 |
222 | style Signal fill:#e1f5ff
223 | style Computed fill:#fff3e0
224 | style Effect fill:#f3e5f5
225 | style Link1 fill:#c8e6c9
226 | style Link2 fill:#c8e6c9
227 | style Link3 fill:#c8e6c9
228 | style Link4 fill:#ffccbc
229 | style Link5 fill:#ffccbc
230 | ```
231 |
232 | **How it works**:
233 | 1. **Dependency Collection**: Effect executes → accesses Signal → creates Link node → adds to both Signal's subs list and Effect's deps list
234 | 2. **Notification Propagation**: Signal changes → traverses subs list → notifies all subscribers to execute
235 | 3. **Dependency Cleanup**: Before Effect re-executes → traverses deps list → removes itself from old dependencies
236 |
237 | ### Link Process
238 |
239 | **Principle**: When Effect executes and accesses Signal, the system automatically establishes dependency relationship.
240 |
241 | ```mermaid
242 | sequenceDiagram
243 | participant E as Effect
244 | participant S as Signal
245 | participant L as Link Node
246 |
247 | Note over E,S: Initial state: No connection
248 | E->>S: Access Signal value
249 | S->>S: Detect activeSub=Effect
250 | S->>L: Create new Link node
251 | L->>S: Add to Signal.subs list
252 | L->>E: Add to Effect.deps list
253 | Note over E,S: Dependency established
254 |
255 | rect rgb(200, 230, 201)
256 | Note right of L: Link structure:
dep=Signal
sub=Effect
bidirectional pointers
257 | end
258 | ```
259 |
260 | **Key Steps**:
261 | 1. **Detect Access**: When Signal is read, check global activeSub
262 | 2. **Create Link**: If activeSub exists, create Link node connecting them
263 | 3. **Prevent Duplicates**: Check if dependency already exists to avoid duplication
264 | 4. **Bidirectional Connection**: Link added to both Signal.subs and Effect.deps
265 |
266 | ### Unlink Process
267 |
268 | **Principle**: When Effect re-executes or is destroyed, old dependency relationships need to be cleaned up.
269 |
270 | ```mermaid
271 | sequenceDiagram
272 | participant E as Effect
273 | participant L as Link Node
274 | participant S as Signal
275 |
276 | Note over E,S: Established dependency
277 | E->>E: Re-execute/Destroy
278 | E->>L: Traverse deps list
279 | L->>S: Remove from Signal.subs
280 | L->>E: Remove from Effect.deps
281 | L->>L: Destroy Link node
282 | Note over E,S: Dependency cleaned up
283 | ```
284 |
285 | **Key Steps**:
286 | 1. **Trigger Timing**: Before Effect re-executes or when destroyed
287 | 2. **Traverse Dependencies**: Find all Link nodes through deps list
288 | 3. **Bidirectional Removal**: Remove from both Signal.subs and Effect.deps
289 | 4. **Memory Release**: Link node is garbage collected
290 |
291 | ### Complex Scenario Example
292 |
293 | **Principle**: The reactive system supports multi-level dependency relationships, forming a Directed Acyclic Graph (DAG).
294 |
295 | ```mermaid
296 | graph LR
297 | A[Signal A] -->|notify| E1[Effect 1]
298 | A -->|notify| C[Computed C]
299 | E1 -->|read| B[Signal B]
300 | B -->|notify| E2[Effect 2]
301 | B -->|notify| C
302 | C -->|notify| E3[Effect 3]
303 | C -->|read| D[Signal D]
304 |
305 | style A fill:#e1f5ff
306 | style B fill:#e1f5ff
307 | style D fill:#e1f5ff
308 | style C fill:#fff3e0
309 | style E1 fill:#f3e5f5
310 | style E2 fill:#f3e5f5
311 | style E3 fill:#f3e5f5
312 | ```
313 |
314 | **Data Flow**:
315 | 1. **Signal A** changes → triggers **Effect 1** and **Computed C**
316 | 2. **Effect 1** executes → may modify **Signal B**
317 | 3. **Signal B** changes → triggers **Effect 2** and **Computed C** (again)
318 | 4. **Computed C** updates → triggers **Effect 3**
319 |
320 | **Optimization Mechanisms**:
321 | - **Dirty Checking**: Computed only recalculates when dependencies change
322 | - **Batch Updates**: Multiple Signals change simultaneously, Effect executes only once
323 | - **Topological Sort**: Ensures dependencies update in correct order, avoiding redundant calculations
324 |
325 | This complex dependency relationship is efficiently managed through the doubly-linked list structure, achieving O(1) time complexity for dependency operations.
326 |
327 | ## Considerations
328 |
329 | 1. Performance Optimization
330 | - Avoid accessing too many reactive values in a single computed property
331 | - Use batch updates judiciously to improve performance
332 | - Don't modify other reactive values inside computed properties
333 |
334 | 2. Circular Dependencies
335 | - Although the system can intelligently handle some circular dependencies
336 | - It's still recommended to avoid complex circular dependencies
337 | - Uses bit flags to prevent infinite recursion and stack overflow
338 |
339 | 3. Memory Management
340 | - System automatically manages dependency relationships
341 | - Effects no longer in use are automatically cleaned up
342 | - Use effectScope to manage multiple effects in complex components
343 |
344 | 4. Lua 5.1 Compatibility
345 | - Supports Lua 5.1
346 | - All examples and tests are compatible with both Lua 5.1 and newer versions
347 |
348 | ## Complete API Reference
349 |
350 | ```lua
351 | local reactive = require("alien_signals")
352 |
353 | -- Core reactive primitives
354 | local signal = reactive.signal -- Create reactive signal
355 | local computed = reactive.computed -- Create computed value
356 | local effect = reactive.effect -- Create reactive effect
357 | local effectScope = reactive.effectScope -- Create effect scope
358 |
359 | -- Batch operation utilities
360 | local startBatch = reactive.startBatch -- Start batch updates
361 | local endBatch = reactive.endBatch -- End batch updates and flush
362 |
363 | -- Advanced control API
364 | local setActiveSub = reactive.setActiveSub -- Set current active subscriber
365 | local getActiveSub = reactive.getActiveSub -- Get current active subscriber
366 | local getBatchDepth = reactive.getBatchDepth -- Get batch update depth
367 |
368 | -- Type detection API
369 | local isSignal = reactive.isSignal -- Check if value is Signal
370 | local isComputed = reactive.isComputed -- Check if value is Computed
371 | local isEffect = reactive.isEffect -- Check if value is Effect
372 | local isEffectScope = reactive.isEffectScope -- Check if value is EffectScope
373 | ```
374 |
375 | ## License
376 |
377 | This project is licensed under the [LICENSE](LICENSE).
378 |
--------------------------------------------------------------------------------
/alien_signals.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | * Alien Signals - A reactive programming system for Lua
3 | * Alien Signals - Lua 响应式编程系统
4 | *
5 | * Version: 3.1.1 (compatible with alien-signals v3.1.1)
6 | * 版本: 3.1.1 (兼容 alien-signals v3.1.1)
7 | *
8 | * Derived from https://github.com/stackblitz/alien-signals
9 | * 源自 https://github.com/stackblitz/alien-signals
10 | *
11 | * This module implements a full-featured reactivity system with automatic
12 | * dependency tracking and efficient updates propagation. It provides:
13 | * - Reactive signals (mutable reactive values)
14 | * - Computed values (derived reactive values)
15 | * - Effects (side effects that run when dependencies change)
16 | * - Effect scopes (grouping and cleanup of multiple effects)
17 | * - Batch updates (efficient bulk state changes)
18 | * - Manual triggering (trigger updates for mutated values)
19 | *
20 | * 该模块实现了一个功能完整的响应式系统,具有自动依赖跟踪和高效的更新传播。它提供:
21 | * - 响应式信号(可变的响应式值)
22 | * - 计算值(派生的响应式值)
23 | * - 副作用(当依赖变化时运行的副作用)
24 | * - 副作用作用域(多个副作用的分组和清理)
25 | * - 批量更新(高效的批量状态变更)
26 | * - 手动触发(为已修改的值触发更新)
27 | ]]
28 |
29 | local bit = require("bit")
30 |
31 | local reactive = {}
32 |
33 | --[[
34 | * Type markers for reactive primitives
35 | * 响应式原语的类型标记
36 | *
37 | * These unique markers are used to identify the type of reactive primitive.
38 | * 这些唯一标记用于识别响应式原语的类型。
39 | ]]
40 | local SIGNAL_MARKER = {}
41 | local COMPUTED_MARKER = {}
42 | local EFFECT_MARKER = {}
43 | local EFFECTSCOPE_MARKER = {}
44 |
45 | --[[
46 | * Simple function binding utility
47 | * 简单的函数绑定工具
48 | *
49 | * This creates a closure that binds the first argument to a function,
50 | * enabling object-oriented-like behavior for reactive primitives.
51 | * 这创建一个闭包,将第一个参数绑定到函数,
52 | * 为响应式原语启用类似面向对象的行为。
53 | ]]
54 | local function bind(func, obj)
55 | return function(...)
56 | return func(obj, ...)
57 | end
58 | end
59 |
60 | --[[
61 | * Bit flags used to track the state of reactive objects
62 | * 用于跟踪响应式对象状态的位标志
63 | *
64 | * These flags use bitwise operations for efficient state management.
65 | * Multiple flags can be combined using bitwise OR operations.
66 | * 这些标志使用位运算进行高效的状态管理。
67 | * 多个标志可以使用位或运算组合。
68 | ]]
69 | local ReactiveFlags = {
70 | None = 0, -- 0000000: Default state / 默认状态
71 | Mutable = 1, -- 0000001: Can be changed (signals and computed values) / 可变的(信号和计算值)
72 | Watching = 2, -- 0000010: Actively watching for changes (effects) / 主动监听变化(副作用)
73 | RecursedCheck = 4, -- 0000100: Being checked for circular dependencies / 正在检查循环依赖
74 | Recursed = 8, -- 0001000: Has been visited during recursion check / 在递归检查中已被访问
75 | Dirty = 16, -- 0010000: Value has changed and needs update / 值已改变需要更新
76 | Pending = 32, -- 0100000: Might be dirty, needs checking / 可能是脏的,需要检查
77 | }
78 |
79 | --[[
80 | * Global state for tracking current active subscriber
81 | * 用于跟踪当前活动订阅者的全局状态
82 | *
83 | * This variable maintains the execution context during reactive operations.
84 | * It enables automatic dependency collection when signals are accessed.
85 | * 该变量在响应式操作期间维护执行上下文。
86 | * 它在访问信号时启用自动依赖收集。
87 | ]]
88 | local g_activeSub = nil -- Current active effect or computed value / 当前活动的副作用或计算值
89 |
90 | --[[
91 | * Queue for batched effect execution
92 | * 批量副作用执行的队列
93 | *
94 | * Effects are queued and executed together for better performance.
95 | * This prevents redundant executions when multiple dependencies change.
96 | * 副作用被排队并一起执行以获得更好的性能。
97 | * 这防止了当多个依赖变化时的冗余执行。
98 | ]]
99 | local g_queued = {} -- Effects waiting to be executed / 等待执行的副作用
100 | local g_queuedLength = 0 -- Length of the queue / 队列长度
101 |
102 | --[[
103 | * Batch update state
104 | * 批量更新状态
105 | *
106 | * Batch operations allow multiple state changes to be grouped together,
107 | * with effects only running once at the end of the batch.
108 | * 批量操作允许将多个状态变更分组在一起,
109 | * 副作用只在批量结束时运行一次。
110 | ]]
111 | local g_batchDepth = 0 -- Depth of nested batch operations / 嵌套批量操作的深度
112 | local g_notifyIndex = 0 -- Current position in the effects queue / 副作用队列中的当前位置
113 |
114 | --[[
115 | * Global version counter for dependency link deduplication
116 | * 用于依赖链接去重的全局版本计数器
117 | *
118 | * This counter is incremented at the start of each tracking cycle to ensure
119 | * that dependency links created in the same cycle can be properly deduplicated.
120 | * 该计数器在每个跟踪周期开始时递增,以确保在同一周期内创建的依赖链接可以正确去重。
121 | ]]
122 | local g_currentVersion = 0 -- Global current version counter for link deduplication / 用于链接去重的全局当前版本计数器
123 |
124 | --[[
125 | * Sets the current active subscriber (effect or computed) and returns the previous one
126 | * 设置当前活动订阅者(副作用或计算值)并返回之前的订阅者
127 | *
128 | * @param sub: New subscriber to set as active / 要设置为活动的新订阅者
129 | * @return: Previous active subscriber / 之前的活动订阅者
130 | *
131 | * This function manages the execution context stack. When a reactive function
132 | * (effect or computed) runs, it becomes the active subscriber, allowing any
133 | * signals accessed during its execution to automatically register as dependencies.
134 | *
135 | * 该函数管理执行上下文栈。当响应式函数(副作用或计算值)运行时,
136 | * 它成为活动订阅者,允许在其执行期间访问的任何信号自动注册为依赖。
137 | ]]
138 | function reactive.setActiveSub(sub)
139 | local prevSub = g_activeSub
140 | g_activeSub = sub
141 | return prevSub
142 | end
143 |
144 | --[[
145 | * Gets the current active subscriber (effect or computed)
146 | * 获取当前活动订阅者(副作用或计算值)
147 | *
148 | * @return: Current active subscriber / 当前活动订阅者
149 | ]]
150 | function reactive.getActiveSub()
151 | return g_activeSub
152 | end
153 |
154 | --[[
155 | * Gets the current batch depth
156 | * 获取当前批量深度
157 | *
158 | * @return: Current batch depth / 当前批量深度
159 | ]]
160 | function reactive.getBatchDepth()
161 | return g_batchDepth
162 | end
163 |
164 | --[[
165 | * Starts a batch update - effects won't be executed until endBatch is called
166 | * 开始批量更新 - 副作用不会执行直到调用 endBatch
167 | *
168 | * This is useful for multiple updates that should be treated as one atomic operation.
169 | * Batching prevents intermediate effect executions and improves performance.
170 | * Batch operations can be nested - effects only run when the outermost batch ends.
171 | *
172 | * 这对于应该被视为一个原子操作的多个更新很有用。
173 | * 批处理防止中间副作用执行并提高性能。
174 | * 批量操作可以嵌套 - 副作用只在最外层批量结束时运行。
175 | ]]
176 | function reactive.startBatch()
177 | g_batchDepth = g_batchDepth + 1
178 | end
179 |
180 | --[[
181 | * Ends a batch update and flushes pending effects if this is the outermost batch
182 | * 结束批量更新,如果这是最外层批量则刷新待处理的副作用
183 | *
184 | * When the batch depth reaches zero, all queued effects are executed.
185 | * This ensures that effects only run once per batch, even if multiple
186 | * dependencies changed during the batch.
187 | *
188 | * 当批量深度达到零时,所有排队的副作用都会被执行。
189 | * 这确保副作用每批只运行一次,即使在批量期间多个依赖发生了变化。
190 | ]]
191 | function reactive.endBatch()
192 | g_batchDepth = g_batchDepth - 1
193 | if 0 == g_batchDepth then
194 | reactive.flush()
195 | end
196 | end
197 |
198 | --[[
199 | * Executes all queued effects
200 | * 执行所有排队的副作用
201 | *
202 | * This is called automatically when a batch update ends or when a signal
203 | * changes outside of a batch. It processes the effect queue in order,
204 | * clearing the queued flag and running each effect.
205 | *
206 | * 这在批量更新结束时或信号在批量外部变化时自动调用。
207 | * 它按顺序处理副作用队列,清除排队标志并运行每个副作用。
208 | ]]
209 | function reactive.flush()
210 | while g_notifyIndex < g_queuedLength do
211 | local effect = g_queued[g_notifyIndex+1]
212 | g_queued[g_notifyIndex+1] = nil
213 | g_notifyIndex = g_notifyIndex + 1
214 |
215 | if effect then
216 | reactive.run(effect)
217 | end
218 | end
219 |
220 | -- Reset queue state after processing all effects
221 | -- 处理完所有副作用后重置队列状态
222 | g_notifyIndex = 0
223 | g_queuedLength = 0
224 | end
225 |
226 | --[[
227 | * Runs an effect based on its current state
228 | * 根据当前状态运行副作用
229 | *
230 | * @param e: The effect to run / 要运行的副作用
231 | *
232 | * This function determines whether an effect needs to run based on its flags.
233 | * Effects run when they are dirty (definitely need update) or pending (might need update).
234 | * During execution, the effect becomes the active subscriber to collect new dependencies.
235 | *
236 | * 该函数根据标志确定副作用是否需要运行。
237 | * 副作用在脏(确实需要更新)或待定(可能需要更新)时运行。
238 | * 在执行期间,副作用成为活动订阅者以收集新的依赖。
239 | ]]
240 | function reactive.run(e)
241 | local flags = e.flags
242 | local isDirty = bit.band(flags, ReactiveFlags.Dirty) > 0
243 |
244 | -- Check if we need to run the effect
245 | -- 检查是否需要运行副作用
246 | local shouldRun = false
247 | repeat
248 | if isDirty then
249 | shouldRun = true
250 | break
251 | end
252 |
253 | local isPending = bit.band(flags, ReactiveFlags.Pending) > 0
254 | if not isPending then
255 | break
256 | end
257 |
258 | if reactive.checkDirty(e.deps, e) then
259 | shouldRun = true
260 | end
261 | until true
262 |
263 | if not shouldRun then
264 | -- Restore Watching flag
265 | -- 恢复监视标志
266 | e.flags = ReactiveFlags.Watching
267 | return
268 | end
269 |
270 | -- Increment global version counter for this tracking cycle
271 | -- 为此跟踪周期递增全局版本计数器
272 | g_currentVersion = g_currentVersion + 1
273 |
274 | -- Reset dependency tail to collect dependencies from scratch
275 | -- 重置依赖尾部以从头收集依赖
276 | e.depsTail = nil
277 |
278 | -- Set flags: Watching | RecursedCheck (2 | 4 = 6)
279 | -- 设置标志:监视 | 递归检查
280 | e.flags = 6
281 |
282 | -- Track effect execution to collect dependencies
283 | -- 跟踪副作用执行以收集依赖
284 | local prev = reactive.setActiveSub(e)
285 |
286 | -- Execute the effect function safely
287 | -- 安全地执行副作用函数
288 | local result, err = pcall(e.fn)
289 | if not result then
290 | print("Error in effect: " .. err)
291 | end
292 |
293 | -- Restore previous state and finish tracking
294 | -- 恢复之前的状态并完成跟踪
295 | g_activeSub = prev
296 |
297 | -- Clear the recursion check flag
298 | -- 清除递归检查标志
299 | e.flags = bit.band(e.flags, bit.bnot(ReactiveFlags.RecursedCheck))
300 |
301 | -- Purge stale dependencies
302 | -- 清除陈旧依赖
303 | reactive.purgeDeps(e)
304 | end
305 |
306 | --[[
307 | * Creates a dependency link node in the doubly-linked list
308 | * 在双向链表中创建依赖链接节点
309 | *
310 | * @param dep: Dependency (signal or computed) / 依赖(信号或计算值)
311 | * @param sub: Subscriber (effect or computed) / 订阅者(副作用或计算值)
312 | * @param prevSub, nextSub: Previous and next links in the subscriber chain / 订阅者链中的前一个和下一个链接
313 | * @param prevDep, nextDep: Previous and next links in the dependency chain / 依赖链中的前一个和下一个链接
314 | * @return: New link object / 新的链接对象
315 | *
316 | * Each link exists in two doubly-linked lists simultaneously:
317 | * 1. The dependency's subscriber list (vertical, linking all subscribers of a dependency)
318 | * 2. The subscriber's dependency list (horizontal, linking all dependencies of a subscriber)
319 | *
320 | * This dual-list structure enables efficient traversal and cleanup operations.
321 | *
322 | * 每个链接同时存在于两个双向链表中:
323 | * 1. 依赖的订阅者列表(垂直,链接依赖的所有订阅者)
324 | * 2. 订阅者的依赖列表(水平,链接订阅者的所有依赖)
325 | *
326 | * 这种双列表结构使得遍历和清理操作更加高效。
327 | ]]
328 | function reactive.createLink(dep, sub, prevSub, nextSub, prevDep, nextDep)
329 | return {
330 | version = 0, -- Version number for deduplication / 用于去重的版本号
331 | dep = dep, -- The dependency object / 依赖对象
332 | sub = sub, -- The subscriber object / 订阅者对象
333 | prevSub = prevSub, -- Previous link in the subscriber's chain / 订阅者链中的前一个链接
334 | nextSub = nextSub, -- Next link in the subscriber's chain / 订阅者链中的下一个链接
335 | prevDep = prevDep, -- Previous link in the dependency's chain / 依赖链中的前一个链接
336 | nextDep = nextDep -- Next link in the dependency's chain / 依赖链中的下一个链接
337 | }
338 | end
339 |
340 | --[[
341 | * Establishes a dependency relationship between a dependency (dep) and a subscriber (sub)
342 | * 在依赖(dep)和订阅者(sub)之间建立依赖关系
343 | *
344 | * This is the core of the dependency tracking system. It creates bidirectional
345 | * links in the reactive graph, allowing changes to propagate efficiently.
346 | *
347 | * @param dep: The reactive object being depended on (signal or computed) / 被依赖的响应式对象(信号或计算值)
348 | * @param sub: The reactive object depending on it (effect or computed) / 依赖它的响应式对象(副作用或计算值)
349 | *
350 | * The function handles several important cases:
351 | * 1. Avoiding duplicate links for the same dependency
352 | * 2. Managing circular dependency detection during recursion checks
353 | * 3. Maintaining proper doubly-linked list structure
354 | * 4. Optimizing for the common case where dependencies are added in order
355 | *
356 | * 该函数处理几个重要情况:
357 | * 1. 避免为同一依赖创建重复链接
358 | * 2. 在递归检查期间管理循环依赖检测
359 | * 3. 维护正确的双向链表结构
360 | * 4. 为按顺序添加依赖的常见情况进行优化
361 | ]]
362 | function reactive.link(dep, sub, version)
363 | -- Check if this dependency is already the last one in the chain
364 | -- 检查这个依赖是否已经是链中的最后一个
365 | local prevDep = sub.depsTail
366 | if prevDep and prevDep.dep == dep then
367 | return
368 | end
369 |
370 | local nextDep
371 | if prevDep then
372 | nextDep = prevDep.nextDep
373 | else
374 | nextDep = sub.deps
375 | end
376 |
377 | if nextDep and nextDep.dep == dep then
378 | nextDep.version = version
379 | sub.depsTail = nextDep
380 | return
381 | end
382 |
383 | -- Check if the sub is already subscribed to this dependency using version-based deduplication
384 | -- 使用基于版本号的去重检查订阅者是否已经订阅了这个依赖
385 | local prevSub = dep.subsTail
386 | if prevSub and prevSub.version == version and prevSub.sub == sub then
387 | return
388 | end
389 |
390 | -- Create a new link and insert it in both chains
391 | -- 创建新链接并将其插入到两个链中
392 | -- createLink(dep, sub, prevSub, nextSub, prevDep, nextDep)
393 | local newLink = reactive.createLink(dep, sub, prevSub, nil, prevDep, nextDep)
394 | newLink.version = version
395 |
396 | dep.subsTail = newLink -- Add to dependency's subscribers chain / 添加到依赖的订阅者链
397 | sub.depsTail = newLink -- Add to subscriber's dependencies chain / 添加到订阅者的依赖链
398 |
399 | -- Update next and previous pointers for proper doubly-linked list behavior
400 | -- 更新下一个和前一个指针以实现正确的双向链表行为
401 | if nextDep then
402 | nextDep.prevDep = newLink
403 | end
404 |
405 | if prevDep then
406 | prevDep.nextDep = newLink
407 | else
408 | sub.deps = newLink
409 | end
410 |
411 | if prevSub then
412 | prevSub.nextSub = newLink
413 | else
414 | dep.subs = newLink
415 | end
416 | end
417 |
418 | --[[
419 | * Removes a dependency link from both chains
420 | * 从两个链中移除依赖链接
421 | *
422 | * @param link: The link to remove / 要移除的链接
423 | * @param sub: The subscriber (can be provided explicitly or taken from link) / 订阅者(可以显式提供或从链接中获取)
424 | * @return: The next dependency link in the chain / 链中的下一个依赖链接
425 | *
426 | * This function carefully removes a link from both the dependency's subscriber list
427 | * and the subscriber's dependency list, maintaining the integrity of both doubly-linked lists.
428 | * When the last subscriber is removed from a dependency, it triggers cleanup.
429 | *
430 | * 该函数小心地从依赖的订阅者列表和订阅者的依赖列表中移除链接,
431 | * 保持两个双向链表的完整性。当从依赖中移除最后一个订阅者时,会触发清理。
432 | ]]
433 | function reactive.unlink(link, sub)
434 | sub = sub or link.sub
435 |
436 | local dep = link.dep
437 | local prevDep = link.prevDep
438 | local nextDep = link.nextDep
439 | local nextSub = link.nextSub
440 | local prevSub = link.prevSub
441 |
442 | -- Remove from the dependency chain (horizontal)
443 | -- 从依赖链中移除(水平方向)
444 | if nextDep then
445 | nextDep.prevDep = prevDep
446 | else
447 | sub.depsTail = prevDep
448 | end
449 |
450 | if prevDep then
451 | prevDep.nextDep = nextDep
452 | else
453 | sub.deps = nextDep
454 | end
455 |
456 | -- Remove from the subscriber chain (vertical)
457 | -- 从订阅者链中移除(垂直方向)
458 | if nextSub then
459 | nextSub.prevSub = prevSub
460 | else
461 | dep.subsTail = prevSub
462 | end
463 |
464 | if prevSub then
465 | prevSub.nextSub = nextSub
466 | else
467 | dep.subs = nextSub
468 |
469 | -- If this was the last subscriber, notify the dependency it's no longer watched
470 | -- 如果这是最后一个订阅者,通知依赖它不再被监视
471 | if not nextSub then
472 | reactive.unwatched(dep)
473 | end
474 | end
475 |
476 | return nextDep
477 | end
478 |
479 | --[[
480 | * Processes subscriber flags and determines the appropriate action
481 | * 处理订阅者标志并确定适当的操作
482 | *
483 | * @param sub: The subscriber object / 订阅者对象
484 | * @param flags: Current flags of the subscriber / 订阅者的当前标志
485 | * @param link: The link connecting dependency and subscriber / 连接依赖和订阅者的链接
486 | * @return: Updated flags for further processing / 用于进一步处理的更新标志
487 | *
488 | * This function encapsulates the complex flag processing logic that determines
489 | * how a subscriber should respond to dependency changes. It handles various
490 | * states like recursion checking, dirty marking, and pending updates.
491 | *
492 | * 该函数封装了复杂的标志处理逻辑,确定订阅者应如何响应依赖变化。
493 | * 它处理各种状态,如递归检查、脏标记和待处理更新。
494 | ]]
495 | local function processSubscriberFlags(sub, flags, link)
496 | -- Check if subscriber is mutable or watching (flags 1 | 2 = 3)
497 | -- 检查订阅者是否可变或正在监视(标志 1 | 2 = 3)
498 | if not bit.band(flags, 3) then
499 | return ReactiveFlags.None
500 | end
501 |
502 | -- Process different flag combinations
503 | -- 处理不同的标志组合
504 |
505 | -- Case 1: No recursion, dirty, or pending flags (60 = 4|8|16|32)
506 | -- 情况1:没有递归、脏或待处理标志
507 | if bit.band(flags, 60) == 0 then
508 | -- Set pending flag (32)
509 | -- 设置待处理标志
510 | sub.flags = bit.bor(flags, 32)
511 | return flags
512 | end
513 |
514 | -- Case 2: No recursion flags (12 = 4|8)
515 | -- 情况2:没有递归标志
516 | if bit.band(flags, 12) == 0 then
517 | return ReactiveFlags.None
518 | end
519 |
520 | -- Case 3: No RecursedCheck flag (4)
521 | -- 情况3:没有递归检查标志
522 | if bit.band(flags, 4) == 0 then
523 | -- Clear Recursed flag (8) and set Pending flag (32)
524 | -- 清除递归标志并设置待处理标志
525 | sub.flags = bit.bor(bit.band(flags, bit.bnot(8)), 32)
526 | return flags
527 | end
528 |
529 | -- Case 4: No Dirty or Pending flags (48 = 16|32) and valid link
530 | -- 情况4:没有脏或待处理标志且链接有效
531 | if bit.band(flags, 48) == 0 and reactive.isValidLink(link, sub) then
532 | -- Set Recursed and Pending flags (40 = 8|32)
533 | -- 设置递归和待处理标志
534 | sub.flags = bit.bor(flags, 40)
535 | return bit.band(flags, ReactiveFlags.Mutable)
536 | end
537 |
538 | -- Default case: clear all flags
539 | -- 默认情况:清除所有标志
540 | return ReactiveFlags.None
541 | end
542 |
543 | --[[
544 | * Handles the core propagation logic for a single subscriber
545 | * 处理单个订阅者的核心传播逻辑
546 | *
547 | * @param sub: Subscriber to process / 要处理的订阅者
548 | * @param flags: Subscriber's flags / 订阅者的标志
549 | * @param link: Link connecting dependency and subscriber / 连接依赖和订阅者的链接
550 | * @return: Subscriber's children (subs) if propagation should continue / 如果应该继续传播则返回订阅者的子级
551 | ]]
552 | local function handleSubscriberPropagation(sub, flags, link)
553 | -- Process subscriber flags and get updated flags
554 | -- 处理订阅者标志并获取更新的标志
555 | local processedFlags = processSubscriberFlags(sub, flags, link)
556 |
557 | -- Notify if subscriber is watching
558 | -- 如果订阅者正在监视则通知
559 | if bit.band(processedFlags, ReactiveFlags.Watching) > 0 then
560 | reactive.notify(sub)
561 | end
562 |
563 | -- Continue propagation if subscriber is mutable
564 | -- 如果订阅者可变则继续传播
565 | if bit.band(processedFlags, ReactiveFlags.Mutable) > 0 then
566 | return sub.subs
567 | end
568 |
569 | return nil
570 | end
571 |
572 | --[[
573 | * Propagates changes through the dependency graph
574 | * 通过依赖图传播变化
575 | *
576 | * This function traverses the subscriber chain and notifies all affected subscribers
577 | * about dependency changes. It uses a simplified stack-based approach to handle
578 | * nested dependencies efficiently.
579 | *
580 | * 该函数遍历订阅者链并通知所有受影响的订阅者依赖变化。
581 | * 它使用简化的基于栈的方法来高效处理嵌套依赖。
582 | ]]
583 | function reactive.propagate(link)
584 | local next = link.nextSub
585 | local stack = nil
586 |
587 | -- Use repeat...until true to simulate continue statements (classic Lua pattern)
588 | -- 使用repeat...until true模拟continue语句(经典Lua模式)
589 | repeat
590 | repeat
591 | local sub = link.sub
592 | local subSubs = handleSubscriberPropagation(sub, sub.flags, link)
593 |
594 | -- Handle mutable subscribers (exactly matching TypeScript logic)
595 | -- 处理可变订阅者(精确匹配TypeScript逻辑)
596 | if subSubs then
597 | -- const nextSub = (link = subSubs).nextSub;
598 | link = subSubs
599 | local nextSub = subSubs.nextSub
600 | if nextSub then
601 | stack = {value = next, prev = stack}
602 | next = nextSub
603 | end
604 | break -- continue; (equivalent to TypeScript's continue)
605 | end
606 |
607 | -- if ((link = next!) !== undefined)
608 | link = next
609 | if link then
610 | next = link.nextSub
611 | break -- continue; (equivalent to TypeScript's continue)
612 | end
613 |
614 | -- while (stack !== undefined)
615 | while stack do
616 | link = stack.value
617 | stack = stack.prev
618 | if link then
619 | next = link.nextSub
620 | break -- continue top; (go to next iteration)
621 | end
622 | end
623 |
624 | if not link then
625 | return -- break; (exit main loop)
626 | end
627 | until true
628 | until false
629 | end
630 |
631 | -- Begins dependency tracking for a subscriber
632 | -- Called when an effect or computed value is about to execute its function
633 | -- @param sub: The subscriber (effect or computed)
634 | function reactive.startTracking(sub)
635 | -- Increment global version counter for this tracking cycle
636 | -- 为此跟踪周期递增全局版本计数器
637 | g_currentVersion = g_currentVersion + 1
638 |
639 | -- Reset dependency tail to collect dependencies from scratch
640 | sub.depsTail = nil
641 |
642 | -- Clear state flags and set RecursedCheck flag
643 | -- 56: Recursed | Dirty | Pending 4: RecursedCheck
644 | sub.flags = bit.bor(bit.band(sub.flags, bit.bnot(56)), 4)
645 | end
646 |
647 | -- Ends dependency tracking for a subscriber
648 | -- Called after an effect or computed value has executed its function
649 | -- Cleans up stale dependencies that were not accessed this time
650 | -- @param sub: The subscriber (effect or computed)
651 | function reactive.endTracking(sub)
652 | -- Find where to start removing dependencies
653 | local depsTail = sub.depsTail
654 | local toRemove = sub.deps
655 | if depsTail then
656 | toRemove = depsTail.nextDep
657 | end
658 |
659 | -- Remove all dependencies that were not accessed during this execution
660 | while toRemove do
661 | toRemove = reactive.unlink(toRemove, sub)
662 | end
663 |
664 | -- Clear the recursion check flag
665 | sub.flags = bit.band(sub.flags, bit.bnot(ReactiveFlags.RecursedCheck))
666 | end
667 |
668 | --[[
669 | * Purges stale dependencies from a subscriber
670 | * 清除订阅者的陈旧依赖
671 | *
672 | * @param sub: The subscriber (effect or computed) / 订阅者(副作用或计算值)
673 | *
674 | * This function removes dependencies that were not accessed during the last
675 | * execution of a reactive function. It's used after tracking is complete.
676 | *
677 | * 该函数移除在响应式函数最后一次执行期间未访问的依赖。
678 | * 它在跟踪完成后使用。
679 | ]]
680 | function reactive.purgeDeps(sub)
681 | local depsTail = sub.depsTail
682 | local toRemove
683 | if depsTail then
684 | toRemove = depsTail.nextDep
685 | else
686 | toRemove = sub.deps
687 | end
688 |
689 | -- Remove all dependencies that were not accessed during this execution
690 | -- 移除在此次执行中未访问的所有依赖
691 | while toRemove do
692 | toRemove = reactive.unlink(toRemove, sub)
693 | end
694 | end
695 |
696 | --[[
697 | * Processes the stack unwinding phase during dependency checking
698 | * 处理依赖检查期间的栈展开阶段
699 | *
700 | * @param checkDepth: Current check depth / 当前检查深度
701 | * @param sub: Current subscriber being processed / 当前正在处理的订阅者
702 | * @param stack: Stack for managing nested checks / 用于管理嵌套检查的栈
703 | * @param link: Current link being processed / 当前正在处理的链接
704 | * @param dirty: Whether dirty state was found / 是否发现脏状态
705 | * @return: Updated values {checkDepth, sub, stack, link, dirty, shouldGotoTop}
706 | ]]
707 | local function processCheckStackUnwind(checkDepth, sub, stack, link, dirty)
708 | local gototop = false
709 |
710 | while checkDepth > 0 do
711 | local shouldExit = false
712 |
713 | checkDepth = checkDepth - 1
714 | local firstSub = sub.subs
715 | local hasMultipleSubs = firstSub.nextSub ~= nil
716 |
717 | if hasMultipleSubs then
718 | link = stack.value
719 | stack = stack.prev
720 | else
721 | link = firstSub
722 | end
723 |
724 | if dirty then
725 | if reactive.update(sub) then
726 | if hasMultipleSubs then
727 | reactive.shallowPropagate(firstSub)
728 | end
729 | sub = link.sub
730 | shouldExit = true
731 | end
732 | else
733 | sub.flags = bit.band(sub.flags, bit.bnot(ReactiveFlags.Pending))
734 | end
735 |
736 | if not shouldExit then
737 | sub = link.sub
738 | if link.nextDep then
739 | link = link.nextDep
740 | gototop = true
741 | shouldExit = true
742 | else
743 | dirty = false
744 | end
745 | end
746 |
747 | if shouldExit then
748 | break
749 | end
750 | end
751 |
752 | return checkDepth, sub, stack, link, dirty, gototop
753 | end
754 |
755 | --[[
756 | * Processes a single step in the dirty checking phase
757 | * 处理脏值检查阶段的单个步骤
758 | *
759 | * @param link: Current link being processed / 当前正在处理的链接
760 | * @param sub: Current subscriber being processed / 当前正在处理的订阅者
761 | * @param stack: Stack for managing nested checks / 用于管理嵌套检查的栈
762 | * @param checkDepth: Current check depth / 当前检查深度
763 | * @return: Updated values {link, sub, stack, checkDepth, dirty, shouldReturn, shouldContinue}
764 | ]]
765 | local function processDirtyCheckStep(link, sub, stack, checkDepth)
766 | local dep = link.dep
767 | local depFlags = dep.flags
768 |
769 | local dirty = false
770 | local isDirty = bit.band(sub.flags, ReactiveFlags.Dirty) > 0
771 | local bit_mut_or_dirty = bit.bor(ReactiveFlags.Mutable, ReactiveFlags.Dirty)
772 | local bit_mut_or_pending = bit.bor(ReactiveFlags.Mutable, ReactiveFlags.Pending)
773 | local isMutOrDirty = bit.band(depFlags, bit_mut_or_dirty) == bit_mut_or_dirty
774 | local isMutOrPending = bit.band(depFlags, bit_mut_or_pending) == bit_mut_or_pending
775 |
776 | if isDirty then
777 | dirty = true
778 | elseif isMutOrDirty then
779 | if reactive.update(dep) then
780 | local subs = dep.subs
781 | if subs.nextSub then
782 | reactive.shallowPropagate(subs)
783 | end
784 | dirty = true
785 | end
786 | elseif isMutOrPending then
787 | if link.nextSub or link.prevSub then
788 | stack = { value = link, prev = stack }
789 | end
790 |
791 | link = dep.deps
792 | sub = dep
793 | checkDepth = checkDepth + 1
794 | return link, sub, stack, checkDepth, dirty, false, true
795 | end
796 |
797 | if not dirty and link.nextDep then
798 | link = link.nextDep
799 | return link, sub, stack, checkDepth, dirty, false, true
800 | end
801 |
802 | local gototop
803 | checkDepth, sub, stack, link, dirty, gototop = processCheckStackUnwind(checkDepth, sub, stack, link, dirty)
804 |
805 | if not gototop and checkDepth <= 0 then
806 | return link, sub, stack, checkDepth, dirty, true, false
807 | end
808 |
809 | return link, sub, stack, checkDepth, dirty, false, true
810 | end
811 |
812 | function reactive.checkDirty(link, sub)
813 | local stack = nil
814 | local checkDepth = 0
815 |
816 | while true do
817 | local dirty, shouldReturn, shouldContinue
818 | link, sub, stack, checkDepth, dirty, shouldReturn, shouldContinue =
819 | processDirtyCheckStep(link, sub, stack, checkDepth)
820 |
821 | if shouldReturn then
822 | return dirty
823 | end
824 |
825 | if not shouldContinue then
826 | break
827 | end
828 | end
829 |
830 | -- Should never reach here in normal execution, but return false for safety
831 | -- 正常执行不应该到达这里,但为了安全返回false
832 | return false
833 | end
834 |
835 | function reactive.shallowPropagate(link)
836 | repeat
837 | local sub = link.sub
838 | local nextSub = link.nextSub
839 | local subFlags = sub.flags
840 |
841 | -- 48: Pending | Dirty, 32: Pending
842 | if bit.band(subFlags, 48) == 32 then
843 | sub.flags = bit.bor(subFlags, ReactiveFlags.Dirty)
844 |
845 | -- Only notify if Watching flag is set and RecursedCheck is not set
846 | -- 只有在设置了 Watching 标志且未设置 RecursedCheck 时才通知
847 | -- 6: Watching | RecursedCheck, 2: Watching
848 | if bit.band(subFlags, 6) == 2 then
849 | reactive.notify(sub)
850 | end
851 | end
852 |
853 | link = nextSub
854 | until not link
855 | end
856 |
857 | function reactive.isValidLink(checkLink, sub)
858 | -- Simplified implementation: traverse backwards from depsTail (matching TypeScript v2.0.7)
859 | -- 简化实现:从depsTail向前遍历(匹配TypeScript v2.0.7)
860 | local link = sub.depsTail
861 | while link do
862 | if link == checkLink then
863 | return true
864 | end
865 | link = link.prevDep
866 | end
867 | return false
868 | end
869 |
870 | function reactive.updateSignal(signal)
871 | signal.flags = ReactiveFlags.Mutable
872 | if signal.currentValue == signal.pendingValue then
873 | return false
874 | end
875 |
876 | signal.currentValue = signal.pendingValue
877 | return true
878 | end
879 |
880 | function reactive.updateComputed(c)
881 | -- Increment global version counter for this tracking cycle
882 | -- 为此跟踪周期递增全局版本计数器
883 | g_currentVersion = g_currentVersion + 1
884 |
885 | -- Reset dependency tail to collect dependencies from scratch
886 | -- 重置依赖尾部以从头收集依赖
887 | c.depsTail = nil
888 |
889 | -- Set flags: Mutable | RecursedCheck (1 | 4 = 5)
890 | -- 设置标志:可变 | 递归检查
891 | c.flags = 5
892 |
893 | local prevSub = reactive.setActiveSub(c)
894 |
895 | local oldValue = c.value
896 | local newValue = oldValue
897 |
898 | local result, err = pcall(function()
899 | newValue = c.getter(oldValue)
900 | c.value = newValue
901 | end)
902 |
903 | if not result then
904 | print("Error in computed: " .. err)
905 | end
906 |
907 | g_activeSub = prevSub
908 |
909 | -- Clear the recursion check flag
910 | -- 清除递归检查标志
911 | c.flags = bit.band(c.flags, bit.bnot(ReactiveFlags.RecursedCheck))
912 |
913 | -- Purge stale dependencies
914 | -- 清除陈旧依赖
915 | reactive.purgeDeps(c)
916 |
917 | return newValue ~= oldValue
918 | end
919 |
920 | -- Updates a signal or computed value and returns whether the value changed
921 | -- @param signal: Signal or Computed object
922 | -- @return: Boolean indicating whether the value changed
923 | function reactive.update(signal)
924 | if signal.getter then
925 | -- For computed values, use the specialized update function
926 | return reactive.updateComputed(signal)
927 | end
928 |
929 | -- For signals, update directly
930 | return reactive.updateSignal(signal)
931 | end
932 |
933 | --[[
934 | * Called when a node is no longer being watched by any subscribers
935 | * 当节点不再被任何订阅者监视时调用
936 | *
937 | * Cleans up the node's dependencies and performs necessary cleanup operations.
938 | * 清理节点的依赖并执行必要的清理操作。
939 | *
940 | * @param node: Signal, Computed, Effect, or EffectScope object / 信号、计算值、副作用或副作用作用域对象
941 | *
942 | * Different node types require different cleanup strategies:
943 | * - Computed values: Remove all dependencies and mark as dirty for potential recomputation
944 | * - Effects/Scopes: Perform complete cleanup to prevent memory leaks
945 | *
946 | * 不同的节点类型需要不同的清理策略:
947 | * - 计算值:移除所有依赖并标记为脏以便潜在的重新计算
948 | * - 副作用/作用域:执行完整清理以防止内存泄漏
949 | ]]
950 | function reactive.unwatched(node)
951 | if bit.band(node.flags, ReactiveFlags.Mutable) == 0 then
952 | reactive.effectScopeOper(node)
953 | elseif node.depsTail then
954 | node.depsTail = nil
955 | node.flags = bit.bor(ReactiveFlags.Mutable, ReactiveFlags.Dirty)
956 | reactive.purgeDeps(node)
957 | end
958 | end
959 |
960 | --[[
961 | * Queues an effect for execution, ensuring inner effects are notified in correct order
962 | * 将副作用排队执行,确保内部副作用按正确顺序通知
963 | *
964 | * @param effect: Effect object to notify / 要通知的副作用对象
965 | *
966 | * This function implements the new notification system (v3.0.2+) that collects
967 | * all nested inner effects in a chain and inserts them in reverse order.
968 | * This ensures inner effects execute in the same order as non-inner effects.
969 | *
970 | * 该函数实现了新的通知系统(v3.0.2+),它在链中收集所有嵌套的内部副作用
971 | * 并以反向顺序插入它们。这确保内部副作用以与非内部副作用相同的顺序执行。
972 | ]]
973 | function reactive.notify(effect)
974 | local insertIndex = g_queuedLength
975 | local firstInsertedIndex = insertIndex
976 |
977 | -- Collect all inner effects (effects with subs) in a chain
978 | -- 在链中收集所有内部副作用(具有subs的副作用)
979 | repeat
980 | -- Clear the Watching flag
981 | -- 清除监视标志
982 | effect.flags = bit.band(effect.flags, bit.bnot(ReactiveFlags.Watching))
983 |
984 | insertIndex = insertIndex + 1
985 | g_queued[insertIndex] = effect
986 |
987 | -- Move to the next inner effect if it exists and is watching
988 | -- 如果存在下一个内部副作用且正在监视,则移至该副作用
989 | effect = effect.subs and effect.subs.sub or nil
990 | if not effect or bit.band(effect.flags, ReactiveFlags.Watching) == 0 then
991 | break
992 | end
993 | until false
994 |
995 | g_queuedLength = insertIndex
996 |
997 | -- Reverse the collected effects to maintain correct execution order
998 | -- 反转收集的副作用以保持正确的执行顺序
999 | -- Note: Lua arrays are 1-indexed, so we need to adjust indices
1000 | -- 注意:Lua 数组从 1 开始索引,所以需要调整索引
1001 | while firstInsertedIndex < insertIndex - 1 do
1002 | local left = g_queued[firstInsertedIndex + 1]
1003 | g_queued[firstInsertedIndex + 1] = g_queued[insertIndex]
1004 | g_queued[insertIndex] = left
1005 | firstInsertedIndex = firstInsertedIndex + 1
1006 | insertIndex = insertIndex - 1
1007 | end
1008 | end
1009 |
1010 | --[[
1011 | * ================== Signal Implementation ==================
1012 | * ================== 信号实现 ==================
1013 | ]]
1014 |
1015 | --[[
1016 | * Signal operator function - handles both get and set operations
1017 | * 信号操作函数 - 处理获取和设置操作
1018 | *
1019 | * @param this: Signal object / 信号对象
1020 | * @param newValue: New value (for set operation) or nil (for get operation) / 新值(用于设置操作)或 nil(用于获取操作)
1021 | * @return: Current value (for get operation) or nil (for set operation) / 当前值(用于获取操作)或 nil(用于设置操作)
1022 | *
1023 | * This function implements the dual behavior of signals:
1024 | * - When called with a value: acts as a setter, updates the signal and notifies subscribers
1025 | * - When called without arguments: acts as a getter, returns current value and registers dependency
1026 | *
1027 | * 该函数实现了信号的双重行为:
1028 | * - 当使用值调用时:作为设置器,更新信号并通知订阅者
1029 | * - 当不带参数调用时:作为获取器,返回当前值并注册依赖
1030 | ]]
1031 | local function signalOper(this, ...)
1032 | local argCount = select('#', ...)
1033 |
1034 | if argCount > 0 then
1035 | -- Set operation (when called with a value, even if it's nil)
1036 | -- 设置操作(当使用值调用时,即使值是 nil)
1037 | local newValue = select(1, ...)
1038 | if newValue ~= this.pendingValue then
1039 | this.pendingValue = newValue
1040 | this.flags = bit.bor(ReactiveFlags.Mutable, ReactiveFlags.Dirty)
1041 |
1042 | -- Notify subscribers if any
1043 | -- 如果有订阅者则通知它们
1044 | local subs = this.subs
1045 | if subs then
1046 | reactive.propagate(subs)
1047 | -- If not in batch mode, execute effects immediately
1048 | -- 如果不在批量模式下,立即执行副作用
1049 | if g_batchDepth == 0 then
1050 | reactive.flush()
1051 | end
1052 | end
1053 | end
1054 | else
1055 | -- Get operation (when called without arguments)
1056 | -- 获取操作(当不带参数调用时)
1057 | -- Check if the signal needs to be updated (for signals within effects)
1058 | -- 检查信号是否需要更新(对于副作用中的信号)
1059 | if bit.band(this.flags, ReactiveFlags.Dirty) > 0 then
1060 | if reactive.updateSignal(this) then
1061 | local subs = this.subs
1062 | if subs then
1063 | reactive.shallowPropagate(subs)
1064 | end
1065 | end
1066 | end
1067 |
1068 | -- Register this signal as a dependency of the current subscriber, if any
1069 | -- 如果有当前订阅者,将此信号注册为其依赖
1070 | local sub = g_activeSub
1071 | while sub do
1072 | if bit.band(sub.flags, 3) > 0 then -- Mutable | Watching
1073 | reactive.link(this, sub, g_currentVersion)
1074 | break
1075 | end
1076 | sub = sub.subs and sub.subs.sub or nil
1077 | end
1078 |
1079 | return this.currentValue
1080 | end
1081 | end
1082 |
1083 | --[[
1084 | * Creates a new reactive signal
1085 | * 创建新的响应式信号
1086 | *
1087 | * @param initialValue: Initial value for the signal / 信号的初始值
1088 | * @return: A function that can be called to get or set the signal's value / 可以调用以获取或设置信号值的函数
1089 | *
1090 | * Signals are the fundamental building blocks of the reactive system. They store
1091 | * mutable values and automatically notify subscribers when changed. The returned
1092 | * function can be used in two ways:
1093 | * - signal() - returns the current value and registers as dependency
1094 | * - signal(newValue) - sets a new value and triggers updates
1095 | *
1096 | * 信号是响应式系统的基本构建块。它们存储可变值并在更改时自动通知订阅者。
1097 | * 返回的函数可以通过两种方式使用:
1098 | * - signal() - 返回当前值并注册为依赖
1099 | * - signal(newValue) - 设置新值并触发更新
1100 | ]]
1101 | local function signal(initialValue)
1102 | local s = {
1103 | __type = SIGNAL_MARKER, -- Type marker for isSignal / isSignal的类型标记
1104 | currentValue = initialValue, -- Current committed value / 当前已提交的值
1105 | pendingValue = initialValue, -- Pending value to be committed / 待提交的值
1106 | subs = nil, -- Linked list of subscribers (head) / 订阅者链表(头部)
1107 | subsTail = nil, -- Linked list of subscribers (tail) / 订阅者链表(尾部)
1108 | flags = ReactiveFlags.Mutable, -- State flags / 状态标志
1109 | }
1110 |
1111 | -- Return a bound function that can be called as signal() or signal(newValue)
1112 | -- 返回一个绑定函数,可以作为 signal() 或 signal(newValue) 调用
1113 | return bind(signalOper, s)
1114 | end
1115 |
1116 | --[[
1117 | * ================== Computed Implementation ==================
1118 | * ================== 计算值实现 ==================
1119 | ]]
1120 |
1121 | --[[
1122 | * Computed operator function - evaluates the computed value when accessed
1123 | * 计算值操作函数 - 在访问时评估计算值
1124 | *
1125 | * @param this: Computed object / 计算值对象
1126 | * @return: Current computed value / 当前计算值
1127 | *
1128 | * Computed values are lazy - they only recalculate when accessed and when their
1129 | * dependencies have changed. This function implements the caching and dependency
1130 | * checking logic that makes computed values efficient.
1131 | *
1132 | * 计算值是惰性的 - 它们只在被访问且依赖发生变化时重新计算。
1133 | * 该函数实现了使计算值高效的缓存和依赖检查逻辑。
1134 | ]]
1135 | local function computedOper(this)
1136 | local flags = this.flags
1137 | local isDirty = bit.band(flags, ReactiveFlags.Dirty) > 0
1138 |
1139 | -- Check if we need to recalculate
1140 | -- 检查是否需要重新计算
1141 | local shouldUpdate = false
1142 | repeat
1143 | if isDirty then
1144 | shouldUpdate = true
1145 | break
1146 | end
1147 |
1148 | local maybeDirty = bit.band(flags, ReactiveFlags.Pending) > 0
1149 | if not maybeDirty then
1150 | break
1151 | end
1152 |
1153 | if reactive.checkDirty(this.deps, this) then
1154 | shouldUpdate = true
1155 | else
1156 | this.flags = bit.band(flags, bit.bnot(ReactiveFlags.Pending))
1157 | end
1158 | until true
1159 |
1160 | -- Recalculate value if it's dirty or possibly dirty (needs checking)
1161 | -- 如果是脏的或可能是脏的(需要检查),则重新计算值
1162 | if shouldUpdate then
1163 | if reactive.updateComputed(this) then
1164 | -- Notify subscribers if value changed
1165 | -- 如果值发生变化,通知订阅者
1166 | local subs = this.subs
1167 | if subs then
1168 | reactive.shallowPropagate(subs)
1169 | end
1170 | end
1171 | elseif flags == 0 then
1172 | -- First access: initialize the computed value (v3.1.0+)
1173 | -- 首次访问:初始化计算值(v3.1.0+)
1174 | -- Set Mutable and RecursedCheck flags to prevent recursion on first run
1175 | -- 设置 Mutable 和 RecursedCheck 标志以防止首次运行时递归
1176 | this.flags = bit.bor(ReactiveFlags.Mutable, ReactiveFlags.RecursedCheck)
1177 | local prevSub = reactive.setActiveSub(this)
1178 | local success, result = pcall(function()
1179 | return this.getter()
1180 | end)
1181 | g_activeSub = prevSub
1182 | -- Clear RecursedCheck flag after first run
1183 | -- 首次运行后清除 RecursedCheck 标志
1184 | this.flags = bit.band(this.flags, bit.bnot(ReactiveFlags.RecursedCheck))
1185 | if success then
1186 | this.value = result
1187 | else
1188 | print("Error in computed initialization: " .. result)
1189 | end
1190 | end
1191 |
1192 | if g_activeSub then
1193 | reactive.link(this, g_activeSub, g_currentVersion)
1194 | end
1195 |
1196 | return this.value
1197 | end
1198 |
1199 | --[[
1200 | * Creates a new computed value
1201 | * 创建新的计算值
1202 | *
1203 | * @param getter: Function that calculates the computed value / 计算计算值的函数
1204 | * @return: A function that returns the computed value when called / 调用时返回计算值的函数
1205 | *
1206 | * Computed values derive their value from other reactive sources (signals or other
1207 | * computed values). They automatically track their dependencies and only recalculate
1208 | * when those dependencies change. This provides efficient derived state management.
1209 | *
1210 | * 计算值从其他响应式源(信号或其他计算值)派生其值。它们自动跟踪其依赖
1211 | * 并仅在这些依赖发生变化时重新计算。这提供了高效的派生状态管理。
1212 | ]]
1213 | local function computed(getter)
1214 | local c = {
1215 | __type = COMPUTED_MARKER, -- Type marker for isComputed / isComputed的类型标记
1216 | value = nil, -- Cached value / 缓存值
1217 | subs = nil, -- Linked list of subscribers (head) / 订阅者链表(头部)
1218 | subsTail = nil, -- Linked list of subscribers (tail) / 订阅者链表(尾部)
1219 | deps = nil, -- Dependencies linked list (head) / 依赖链表(头部)
1220 | depsTail = nil, -- Dependencies linked list (tail) / 依赖链表(尾部)
1221 | flags = ReactiveFlags.None, -- Start with no flags (will be initialized on first access) / 从无标志开始(将在首次访问时初始化)
1222 | getter = getter, -- Function to compute the value / 计算值的函数
1223 | }
1224 |
1225 | -- Return a bound function that can be called to get the computed value
1226 | -- 返回一个绑定函数,可以调用以获取计算值
1227 | return bind(computedOper, c)
1228 | end
1229 |
1230 |
1231 | --[[
1232 | * ================== Effect Implementation ==================
1233 | * ================== 副作用实现 ==================
1234 | ]]
1235 |
1236 | --[[
1237 | * Effect scope cleanup operator - stops an effect scope
1238 | * 副作用作用域清理操作符 - 停止副作用作用域
1239 | *
1240 | * @param this: EffectScope object / 副作用作用域对象
1241 | * @return: nil
1242 | *
1243 | * This function performs cleanup of an effect scope:
1244 | * 1. Removes all dependency links
1245 | * 2. Unlinks from parent scopes if any
1246 | *
1247 | * 该函数执行副作用作用域的清理:
1248 | * 1. 移除所有依赖链接
1249 | * 2. 如果有的话,从父作用域取消链接
1250 | ]]
1251 | function reactive.effectScopeOper(this)
1252 | -- Clear depsTail and flags
1253 | -- 清除 depsTail 和 flags
1254 | this.depsTail = nil
1255 | this.flags = ReactiveFlags.None
1256 |
1257 | -- Unlink all dependencies using purgeDeps
1258 | -- 使用 purgeDeps 取消所有依赖的链接
1259 | reactive.purgeDeps(this)
1260 |
1261 | -- If this effect/scope is a dependency for other effects, unlink it
1262 | -- 如果此副作用/作用域是其他副作用的依赖,取消其链接
1263 | local sub = this.subs
1264 | if sub then
1265 | reactive.unlink(sub)
1266 | end
1267 | end
1268 |
1269 | --[[
1270 | * Effect cleanup operator - stops an effect
1271 | * 副作用清理操作符 - 停止副作用
1272 | *
1273 | * @param this: Effect object / 副作用对象
1274 | * @return: nil
1275 | *
1276 | * This function performs complete cleanup of an effect:
1277 | * 1. Removes all dependency links to prevent memory leaks
1278 | * 2. Unlinks from parent effects/scopes if any
1279 | * 3. Clears all state flags to mark as inactive
1280 | *
1281 | * 该函数执行副作用的完整清理:
1282 | * 1. 移除所有依赖链接以防止内存泄漏
1283 | * 2. 如果有的话,从父副作用/作用域取消链接
1284 | * 3. 清除所有状态标志以标记为非活动
1285 | ]]
1286 | local function effectOper(this)
1287 | reactive.effectScopeOper(this)
1288 | this.flags = ReactiveFlags.None
1289 | end
1290 | reactive.effectOper = effectOper
1291 |
1292 | --[[
1293 | * Creates a reactive effect that runs immediately and re-runs when dependencies change
1294 | * 创建响应式副作用,立即运行并在依赖变化时重新运行
1295 | *
1296 | * @param fn: Function to execute reactively / 要响应式执行的函数
1297 | * @return: A cleanup function that stops the effect when called / 调用时停止副作用的清理函数
1298 | *
1299 | * Effects are the bridge between the reactive system and the outside world.
1300 | * They automatically track their dependencies during execution and re-run
1301 | * whenever those dependencies change. Effects are useful for:
1302 | * - DOM updates
1303 | * - API calls
1304 | * - Logging and debugging
1305 | * - Any side effect that should respond to state changes
1306 | *
1307 | * 副作用是响应式系统与外部世界之间的桥梁。
1308 | * 它们在执行期间自动跟踪其依赖,并在这些依赖发生变化时重新运行。
1309 | * 副作用适用于:
1310 | * - DOM 更新
1311 | * - API 调用
1312 | * - 日志记录和调试
1313 | * - 任何应该响应状态变化的副作用
1314 | ]]
1315 | local function effect(fn)
1316 | -- Create the effect object
1317 | -- 创建副作用对象
1318 | local e = {
1319 | __type = EFFECT_MARKER, -- Type marker for isEffect / isEffect的类型标记
1320 | fn = fn, -- The effect function / 副作用函数
1321 | subs = nil, -- Subscribers (if this effect is a dependency) / 订阅者(如果此副作用是依赖)
1322 | subsTail = nil, -- End of subscribers list / 订阅者列表的末尾
1323 | deps = nil, -- Dependencies linked list (head) / 依赖链表(头部)
1324 | depsTail = nil, -- Dependencies linked list (tail) / 依赖链表(尾部)
1325 | -- Mark as watching and set RecursedCheck to prevent recursion on first run
1326 | -- 标记为监视并设置 RecursedCheck 以防止首次运行时递归
1327 | flags = bit.bor(ReactiveFlags.Watching, ReactiveFlags.RecursedCheck),
1328 | }
1329 |
1330 | -- Set this effect as active subscriber and link to parent if any
1331 | -- 将此副作用设置为活动订阅者,如果有父级则链接到父级
1332 | local prevSub = reactive.setActiveSub(e)
1333 | if prevSub then
1334 | reactive.link(e, prevSub, 0)
1335 | end
1336 |
1337 | -- Run the effect for the first time, collecting dependencies
1338 | -- 第一次运行副作用,收集依赖
1339 | local success, err = pcall(fn)
1340 |
1341 | -- Restore previous subscriber
1342 | -- 恢复之前的订阅者
1343 | g_activeSub = prevSub
1344 |
1345 | -- Clear RecursedCheck flag after first run
1346 | -- 首次运行后清除 RecursedCheck 标志
1347 | e.flags = bit.band(e.flags, bit.bnot(ReactiveFlags.RecursedCheck))
1348 |
1349 | if not success then
1350 | error(err)
1351 | end
1352 |
1353 | -- Return the cleanup function
1354 | -- 返回清理函数
1355 | return bind(effectOper, e)
1356 | end
1357 |
1358 | --[[
1359 | * Creates a scope that collects multiple effects and provides a single cleanup function
1360 | * 创建收集多个副作用并提供单个清理函数的作用域
1361 | *
1362 | * @param fn: Function that creates effects within the scope / 在作用域内创建副作用的函数
1363 | * @return: A cleanup function that stops all effects in the scope when called / 调用时停止作用域内所有副作用的清理函数
1364 | *
1365 | * Effect scopes provide a way to group related effects together for easier management.
1366 | * When the scope is cleaned up, all effects created within it are automatically
1367 | * cleaned up as well. This is particularly useful for:
1368 | * - Component lifecycle management
1369 | * - Feature modules that need cleanup
1370 | * - Temporary reactive contexts
1371 | *
1372 | * 副作用作用域提供了将相关副作用分组在一起以便更容易管理的方法。
1373 | * 当作用域被清理时,在其中创建的所有副作用也会自动清理。
1374 | * 这对以下情况特别有用:
1375 | * - 组件生命周期管理
1376 | * - 需要清理的功能模块
1377 | * - 临时响应式上下文
1378 | ]]
1379 | local function effectScope(fn)
1380 | -- Create the effect scope object
1381 | -- 创建副作用作用域对象
1382 | local e = {
1383 | __type = EFFECTSCOPE_MARKER, -- Type marker for isEffectScope / isEffectScope的类型标记
1384 | deps = nil, -- Dependencies linked list (head) / 依赖链表(头部)
1385 | depsTail = nil, -- Dependencies linked list (tail) / 依赖链表(尾部)
1386 | subs = nil, -- Subscribers (child effects) / 订阅者(子副作用)
1387 | subsTail = nil, -- End of subscribers list / 订阅者列表的末尾
1388 | flags = ReactiveFlags.None, -- No special flags needed / 不需要特殊标志
1389 | }
1390 |
1391 | -- Set this scope as active subscriber and link to parent if any
1392 | -- 将此作用域设置为活动订阅者,如果有父级则链接到父级
1393 | local prevSub = reactive.setActiveSub(e)
1394 | if prevSub then
1395 | reactive.link(e, prevSub, 0)
1396 | end
1397 |
1398 | -- Execute the function to create effects within this scope
1399 | -- 执行函数以在此作用域内创建副作用
1400 | local success, err = pcall(function()
1401 | fn()
1402 | end)
1403 |
1404 | -- Restore previous subscriber
1405 | -- 恢复之前的订阅者
1406 | g_activeSub = prevSub
1407 |
1408 | if not success then
1409 | error(err)
1410 | end
1411 |
1412 | -- Return the cleanup function for the entire scope
1413 | -- 返回整个作用域的清理函数
1414 | return bind(reactive.effectScopeOper, e)
1415 | end
1416 |
1417 | --[[
1418 | * Manually triggers updates for dependencies
1419 | * 手动触发依赖更新
1420 | *
1421 | * @param fn: A signal or a function that accesses signals / 一个信号或访问信号的函数
1422 | *
1423 | * The trigger() function allows you to manually trigger updates for downstream
1424 | * dependencies when you've directly mutated a signal's value without using the
1425 | * signal setter. This is useful when working with mutable data structures like
1426 | * arrays or tables.
1427 | *
1428 | * trigger() 函数允许您在直接修改信号值而不使用信号设置器时手动触发下游依赖的更新。
1429 | * 这在处理可变数据结构(如数组或表)时很有用。
1430 | *
1431 | * Usage examples / 使用示例:
1432 | * 1. Trigger a single signal / 触发单个信号:
1433 | * trigger(mySignal)
1434 | *
1435 | * 2. Trigger multiple signals / 触发多个信号:
1436 | * trigger(function()
1437 | * signal1()
1438 | * signal2()
1439 | * end)
1440 | ]]
1441 | local function trigger(fn)
1442 | -- Create a temporary subscriber to collect dependencies
1443 | -- 创建临时订阅者以收集依赖
1444 | local sub = {
1445 | deps = nil,
1446 | depsTail = nil,
1447 | flags = ReactiveFlags.Watching,
1448 | }
1449 |
1450 | local prevSub = reactive.setActiveSub(sub)
1451 |
1452 | -- Execute the function or call the signal to collect dependencies
1453 | -- 执行函数或调用信号以收集依赖
1454 | local success, err = pcall(function()
1455 | fn()
1456 | end)
1457 |
1458 | -- Restore previous subscriber and trigger updates
1459 | -- 恢复之前的订阅者并触发更新
1460 | reactive.setActiveSub(prevSub)
1461 |
1462 | if not success then
1463 | error(err)
1464 | end
1465 |
1466 | -- Trigger updates for all collected dependencies
1467 | -- 为所有收集的依赖触发更新
1468 | local link = sub.deps
1469 | while link do
1470 | local dep = link.dep
1471 |
1472 | -- Unlink this dependency (unlink returns nextDep)
1473 | -- 取消此依赖的链接(unlink 返回 nextDep)
1474 | link = reactive.unlink(link, sub)
1475 |
1476 | -- Propagate updates if the dependency has subscribers
1477 | -- 如果依赖有订阅者,则传播更新
1478 | local subs = dep.subs
1479 | if subs then
1480 | -- Reset flags before propagate to prevent the trigger function sub from being notified
1481 | -- 在传播前重置标志位,防止 trigger 函数的临时订阅者被通知
1482 | sub.flags = ReactiveFlags.None
1483 | reactive.propagate(subs)
1484 | reactive.shallowPropagate(subs)
1485 | end
1486 | end
1487 |
1488 | -- Flush queued effects if not in a batch
1489 | -- 如果不在批处理中,则刷新排队的副作用
1490 | if g_batchDepth == 0 then
1491 | reactive.flush()
1492 | end
1493 | end
1494 |
1495 | --[[
1496 | * Type checking functions
1497 | * 类型检测函数
1498 | *
1499 | * These functions check if a value is a specific reactive primitive type
1500 | * by checking for unique marker properties.
1501 | *
1502 | * 这些函数通过检查唯一标记属性来检查值是否为特定的响应式原语类型。
1503 | ]]
1504 |
1505 | local function isSignal(fn)
1506 | if type(fn) ~= "function" then
1507 | return false
1508 | end
1509 | -- Access the internal structure via upvalues
1510 | -- 通过upvalues访问内部结构
1511 | local i = 1
1512 | while true do
1513 | local name, value = debug.getupvalue(fn, i)
1514 | if not name then break end
1515 | if name == "obj" and type(value) == "table" then
1516 | return value.__type == SIGNAL_MARKER
1517 | end
1518 | i = i + 1
1519 | end
1520 | return false
1521 | end
1522 |
1523 | local function isComputed(fn)
1524 | if type(fn) ~= "function" then
1525 | return false
1526 | end
1527 | local i = 1
1528 | while true do
1529 | local name, value = debug.getupvalue(fn, i)
1530 | if not name then break end
1531 | if name == "obj" and type(value) == "table" then
1532 | return value.__type == COMPUTED_MARKER
1533 | end
1534 | i = i + 1
1535 | end
1536 | return false
1537 | end
1538 |
1539 | local function isEffect(fn)
1540 | if type(fn) ~= "function" then
1541 | return false
1542 | end
1543 | local i = 1
1544 | while true do
1545 | local name, value = debug.getupvalue(fn, i)
1546 | if not name then break end
1547 | if name == "obj" and type(value) == "table" then
1548 | return value.__type == EFFECT_MARKER
1549 | end
1550 | i = i + 1
1551 | end
1552 | return false
1553 | end
1554 |
1555 | local function isEffectScope(fn)
1556 | if type(fn) ~= "function" then
1557 | return false
1558 | end
1559 | local i = 1
1560 | while true do
1561 | local name, value = debug.getupvalue(fn, i)
1562 | if not name then break end
1563 | if name == "obj" and type(value) == "table" then
1564 | return value.__type == EFFECTSCOPE_MARKER
1565 | end
1566 | i = i + 1
1567 | end
1568 | return false
1569 | end
1570 |
1571 | --[[
1572 | * ================== Module Exports ==================
1573 | * ================== 模块导出 ==================
1574 | *
1575 | * This module exports the core reactive primitives and utilities needed
1576 | * to build reactive applications. The API is designed to be simple yet
1577 | * powerful, providing fine-grained reactivity with automatic dependency
1578 | * tracking and efficient update propagation.
1579 | *
1580 | * 该模块导出构建响应式应用程序所需的核心响应式原语和工具。
1581 | * API 设计简单而强大,提供细粒度的响应性,具有自动依赖跟踪和高效的更新传播。
1582 | ]]
1583 | return {
1584 | -- Core reactive primitives / 核心响应式原语
1585 | signal = signal, -- Create a reactive signal / 创建响应式信号
1586 | computed = computed, -- Create a computed value / 创建计算值
1587 | effect = effect, -- Create a reactive effect / 创建响应式副作用
1588 | effectScope = effectScope, -- Create an effect scope / 创建副作用作用域
1589 | trigger = trigger, -- Manually trigger updates / 手动触发更新
1590 |
1591 | -- Type checking / 类型检查
1592 | isSignal = isSignal, -- Check if value is a signal / 检查值是否为信号
1593 | isComputed = isComputed, -- Check if value is a computed / 检查值是否为计算值
1594 | isEffect = isEffect, -- Check if value is an effect / 检查值是否为副作用
1595 | isEffectScope = isEffectScope, -- Check if value is an effect scope / 检查值是否为副作用作用域
1596 |
1597 | -- Batch operation utilities / 批量操作工具
1598 | startBatch = reactive.startBatch, -- Start batch updates / 开始批量更新
1599 | endBatch = reactive.endBatch, -- End batch updates and flush / 结束批量更新并刷新
1600 |
1601 | -- Getter functions / 获取函数
1602 | getActiveSub = reactive.getActiveSub, -- Get current active subscriber / 获取当前活动订阅者
1603 | getBatchDepth = reactive.getBatchDepth, -- Get current batch depth / 获取当前批量深度
1604 |
1605 | -- Advanced API (for internal or advanced usage) / 高级 API(用于内部或高级用法)
1606 | setActiveSub = reactive.setActiveSub, -- Set current subscriber / 设置当前订阅者
1607 | ReactiveFlags = ReactiveFlags, -- Reactive flags constants / 反应式标志常量
1608 | }
--------------------------------------------------------------------------------