vue proxy实现

之前尝试实现vue数据劫持,里面有一些方法实现不是很好,经过查阅#lua.org,实现了基本操作的劫持;

基本数据类型

lua作为一个脚本语言没有封装更多的数据结构,除了原生的numberstringboolean、数据结构,其他数据都是table,和js不同的是 table 没有区分出listmap两种,这里为了方便区分,封装了一个简单区分的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---@param model table
function isArray(model)
-- 连续数据区长度大于0
return #model > 0
end

function isMap(model)
local len = #model
-- 当len为0的时候 next会出错, 默认为true
if len == 0 then
return true
end
-- 除了array区域还有数据,则为map
return next(model, len) ~= nil
end

有了这两个数据类型我们可以按照对 table 的读写操作进行区分:

操作\数据结构 list map
t[key] key为number,且小于#t t[key]或者t.key,key不为number或者key>#t
t[key]=v key为number,且小于#t t[key]=v或者t.key=v key不为number或者key>#t
遍历 ipairs pairs
获取长度 #t 无,可以pairs实现O(n)
has k < #t t[k]~= nil

一般情况下需要劫持的对象就这么多,那么我们可以使用[metatable](Programming in Lua : 13)重写默认的操作符如下:

1、使metatable生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---@param model table{__reactive: boolean | nil}
---@ metatable {__index, __newindex, pairs, ipairs, __len}
local function newProxy(model, metatable)
-- 不是table就没啥好说的了
if type(model) ~= "table" then
return model
end
-- 给proxy做proxy除了浪费没有任何意义
if model.__reactive then
return model
end

-- 存储原来的数据到__raw方便后续处理,也可以用闭包方式
local self = {
__raw = model
}
setmetatable(self, metatable)
return self
end

1、读操作劫持

当当前table找不到对应的key的时候会使用metatable__index方法,这样我们实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 返回处理过的返回结果
-- 当key对应在raw里面的结果是table,返回proxy对象
__index = function(t, key)
if key == "__reactive" then
-- 标记当前对象是否已经是proxy了,给proxy做proxy除了浪费没有任何意义
return true
end

if key == 'raw' then
return t.__raw
end
-- do something
print("try get ".. key)
-- 考虑嵌套的问题,避免返回嵌套的proxy
local value = rawget(t.__raw, key)
if type(value) ~= "table" then
return value
end
-- 使用proxyMap缓存数据,避免同一个key出现多个proxy代理
return proxyMap[value] or proxy(value, metatable)
end

现在读数据方法已经有了,但是需要将写数据的方法也改到metatable实现里面,避免直接使用proxy的属性

2、写操作劫持

当当前table找不到对应的key的时候会使用metatable__newindex方法,这样我们实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__newindex = function(t, key, value)
local oldValue = rawget(t.__raw, key)
rawset(t.__raw, key, value)
-- 当数据发生变化的时候do something
if (oldValue ~= value) then
-- 数组的插入和删除会对后续的元素产生影响,需要单独处理
-- 如t ={1,2,3,4}, table.remove(t, 1)之后t={1,3,4} 对元素而言t[2] 从2变成了3
if isArray(t) and tonumber(key) <= #t then
-- 区分删除还是增加
if oldValue == nil then
print("add a key to array")
end

if value == nil then
print("remove a key to array")
end
else
print("set a key to map")
end
end
return true
end

3、遍历劫持

对于table,会优先使用metatable__pairs__ipairs方法,我们简单实现一下:

1
2
3
4
5
6
7
8
9
10
__pairs = function(t)
--do something
print("pairs")
return pairs(t.__raw)
end
__ipairs = function(t)
--do something
print("ipairs")
return ipairs(t.__raw)
end

4、劫持获取长度操作符

对于table,会优先使用metatable__len,我们简单实现一下:

1
2
3
4
5
__len = function(t)
--do something
print("length")
return #t.__raw
end

综合起来我们的 proxy 实现如下:

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
local proxyMap = {
__model = "k"
}

---@param model table
function isArray(model)
-- 连续数据区长度大于0
return #model > 0
end
---@param model table{__reactive: boolean | nil}
---@ metatable {__index, __newindex, pairs, ipairs, __len}
local function newProxy(model, metatable)
-- 不是table就没啥好说的了
if type(model) ~= "table" then
return model
end
-- 给proxy做proxy除了浪费没有任何意义
if model.__reactive then
return model
end

-- 存储原来的数据到__raw方便后续处理,也可以用闭包方式
local self = {
__raw = model
}
setmetatable(self, metatable)
return self
end

local metatable = {
-- 返回处理过的返回结果
-- 当key对应在raw里面的结果是table,返回proxy对象
__index = function(t, key)
if key == "__reactive" then
-- 标记当前对象是否已经是proxy了,给proxy做proxy除了浪费没有任何意义
return true
end

if key == 'raw' then
return t.__raw
end
-- do something
print("try get " .. key)
-- 考虑嵌套的问题,避免返回嵌套的proxy
local value = rawget(t.__raw, key)
if type(value) ~= "table" then
return value
end
-- 使用proxyMap缓存数据,避免同一个key出现多个proxy代理
return proxyMap[value] or newProxy(value, metatable)
end,
__newindex = function(t, key, value)
local oldValue = rawget(t.__raw, key)
rawset(t.__raw, key, value)
-- 当数据发生变化的时候do something
if (oldValue ~= value) then
-- 数组的插入和删除会对后续的元素产生影响,需要单独处理
-- 如t ={1,2,3,4}, table.remove(t, 1)之后t={1,3,4} 对元素而言t[2] 从2变成了3
if isArray(t) and tonumber(key) <= #t.__raw then
-- 区分删除还是增加
if oldValue == nil then
print("add a key to array")
end

if value == nil then
print("remove a key to array")
end
else
print("set a key to map")
end
end
return true
end,
__pairs = function(t)
-- do something
print("pairs")
return pairs(t.__raw)
end,
__ipairs = function(t)
-- do something
print("ipairs")
return ipairs(t.__raw)
end,
__len = function(t)
-- do something
print("length")
return #t.__raw
end
}

测试代码如下:

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
local model = newProxy({
name = "testname",
age = 13,
info = {
familyNum = 10,
brother = 20,
sonName = "testSon"
}
}, metatable)

print(model.name)
model.info.familyNum = 13
local info = model.info
info.sonName = "newSonName"

table.insert(model, 1)

table.insert(model, 2)
model[2] = nil
table.remove(model, 1)
----- 输出
-- try get name
-- testname
-- try get info
-- try get info
-- length
-- length
-- add a key to array
-- length
-- length
-- add a key to array
-- length
-- set a key to map
-- length
-- try get 1
-- length
-- set a key to map

至此就完成了数据的劫持,并且实现了深度劫持;后续可以实现基于劫持方法实现更加复杂的观察者模式,并且不影响现有的编程习惯,不需要写setget方法;