仿照js proxy实现的`lua`深度数据劫持

104 阅读3分钟

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

基本数据类型

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

---@param model table
function isArray(model)
    -- 连续数据区长度大于0
    return #model > 0
endfunction 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的读写操作进行区分:

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

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

1、使metatable生效

---@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方法,这样我们实现如下:

-- 返回处理过的返回结果
-- 当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方法,这样我们实现如下:

__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方法,我们简单实现一下:

__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,我们简单实现一下:

__len = function(t)
    --do something
    print("length")
    return #t.__raw
end

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

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
endlocal 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
}

测试代码如下:

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方法;