├── .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 | } --------------------------------------------------------------------------------