Lua中实现简单的JavaScript MobX模块

2018-03-01 18:07

JavaScript中有许多好玩有趣的模块,Mobx就是有趣模块的之一,这个模块是由公司的主程推荐使用的。
我在经过一段时间的摸索使用之后了解这个模块的快乐之处。并且对其的实现原理感到了好奇,于是在自己的臆想之下在Lua中实现了主要的几个方法(observable、autorun)。
这里是使用Lua中的元表实现的,对Lua元表不了解的同学建议先了解一下

MobX的介绍

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。了解更多

实现思路

使用observable后将对象、键名、默认值储存起来并将原来对象的值改为nil(如果不改为nil的话,将不会调用到元表的中index与newindex方法),并且重写该对象metatable中的index与newindex。然后在使用autorun之时将方法压入observableStack栈中并调用方法。这时候在index中就能得到访问的键名了,检查这个键名是否存在于observable列表中,如果存在就将目前observableStack栈顶的方法获取下来并根据关系将其储存起来。执行完毕后弹出栈中方法。之后当我们修改值时会在newindex得到相关键值,然后根据键值调用之前储存的相关方法。这就是大概的过程了。

下面是具体代码

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
---@param table any[]
---@param value any
local function indexOf(table, value)
for i, v in ipairs(table) do
if v == value then
return i
end
end
return -1
end

-------------------------------------------------------------------

local observableStack = {}
function observableStack.push(callback)
table.insert(observableStack, callback)
end
function observableStack.pop()
table.remove(observableStack, #observableStack)
end
function observableStack.current()
return observableStack[#observableStack];
end

-------------------------------------------------------------------

---@type table<table,string>
local observables = {}
---@type table<table,string>
local observableValues = {}
---@type table<table,table<string,fun()[]>>
local observableCallbacks = {}

local function hasObservable(caller, key)
if key then
return observables[caller] and indexOf(observables[caller], key) > 0
end
return observables[caller] ~= nil
end

local function callbackObservable(caller, key, value)
if observableCallbacks[caller][key] then
for _, func in ipairs(observableCallbacks[caller][key]) do
func()
end
end
end

local function setAndCallObservable(caller, key, value)
observableValues[caller][key] = value
callbackObservable(caller, key)
end

local function addObservable(caller, key, value)
if hasObservable(caller, key) then
return
end
if not value then
value = caller[key];
end
caller[key] = nil
if not observables[caller] then
observables[caller] = {}
observableValues[caller] = {}
observableCallbacks[caller] = {}
end
table.insert(observables[caller], key)
observableValues[caller][key] = value
if not observableCallbacks[caller][key] then
observableCallbacks[caller][key] = {}
end
end

local function getObservableValue(caller, key)
return observableValues[caller][key]
end

---@param callback func()
---@return func()
local function newDisposer(callback)
return function()
for _, caller in pairs(observableCallbacks) do
for _, key in pairs(caller) do
local index = indexOf(key, callback);
while index > 0 do
table.remove(key, index);
index = indexOf(key, callback);
end
end
end
end
end

---@param caller table
---@param key string
---@param value any
function observable(caller, key, value)
if not hasObservable(caller) then
setmetatable(caller, {
__newindex = function(c, k, v)
if hasObservable(c, k) then
if not (getObservableValue(c, k) == v) then
setAndCallObservable(c, k, v)
end
else
rawset(c, k, v)
end
end,
__index = function(caller, key)
if hasObservable(caller, key) then
local callback = observableStack.current()
if callback then
if indexOf(observableCallbacks[caller][key], callback) <= 0 then
table.insert(observableCallbacks[caller][key], callback)
end
end
return getObservableValue(caller, key)
end
end
})
end
addObservable(caller, key, value)
end

---@param func fun():void
---@return fun():void
function autorun(func)
-- 这里可以针对某些特殊情况开启
if nil then
local function f()
observableStack.push(f);
func();
observableStack.pop();
end
f()
return newDisposer(f);
end
observableStack.push(func);
func()
observableStack.pop();
return newDisposer(func);
end

这里提供两个简单的例子

示例1
1
2
3
4
5
6
7
8
9
10
11
local hero = { hp = 100 }
---将hp设为观测变量
observable(hero, "hp")

---autorun 会默认执行一次
autorun(function()
print(string.format("hero.hp:%d", hero.hp));
end)

---修改值之后会触发执行 autorun 中的方法
hero.hp = 80

它的打印结果为

1
2
> hero.hp:100
> hero.hp:80
示例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local p1 = {}
local p2 = {}
observable(p1, "x", 0 );
observable(p2, "x", 0 );

---autorun 会执行一次提供的方法并且返回一个用于释放的方法
local disponser = autorun(function()
print(string.format("p1.x: %d, p2.x:%d", p1.x, p2.x));
end)

---修改值之后会触发执行 autorun 中的方法
p1.x = 200;
p2.x = 300;

---调用了disponser释放之后
disponser();

---修改值后将不会触发 autorun 中的方法了
p1.x = 100;
p2.x = 100;

它的打印结果为

1
2
3
> p1.x: 0, p2.x:0
> p1.x: 200, p2.x:0
> p1.x: 200, p2.x:300
后记

这里实现的方式其实是非常简单的,只是运行效率不算太友好,并且可能无法支持class,因为Lua的class实现一般会使用到元表,但是这边会改变元表所以可能会造成一些问题。但是目前我暂时还没有想到更好的方案,如果你有更好的思路或者建议的话,可以给我邮件。


标签: lua,

Creative Commons ©fox 2017 - 2018 | Theme based on fzheng.me &