• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    分析Lua观察者模式最佳实践之构建事件分发系统

    一、前言

    试想这样一个问题,当某个事件发生时,比如在游戏中A模块修改了用户的金币数,而B模块和C模块提供的功能都依赖于用户的金币数,那么,A模块在修改金币数的同时,就需要通知B模块和C模块。常规的方法就是A模块持有B模块和C模块的对象,然后分别通过调用对象接口的方式告诉它们,“嘿,我修改了用户的金币数,改成了10金币”。

    但这样就带来了许多问题:

    为了解决上面的问题,我们自然想到了观察者模式。

    二、观察者模式

    这里简单说一下什么是观察者模式:定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者(称之为观察者)都会接收到通知并自动更新。

    观察者模式的好处是,对象之间是松耦合的,当一个对象改变状态时,它并不需要知道自己的观察者是谁,只需要发布通知即可。任何时候都可以增加或删除观察者,不会影响到发布通知的对象。而事件分发系统就是观察者模式的一个具体实现

    三、事件分发系统

    事件分发系统核心需要提供的功能主要包括以下几个部分:

    四、使用事件分发系统解决问题

    首先,来看看使用事件分发系统处理上面提到的问题,会是什么样的效果。

    A模块只需要派发金币修改事件,B,C模块只需要订阅金币修改事件,之后便可以收到通知了。是不是很简单呢

    local B = class()
    function B:on_money_change( money )
        print(money, "B receive event")
    end
    -- 订阅金币修改事件
    EventSystem:on(Event.MoneyChanged, B.on_money_change, {target = B})
    
    local C = class()
    function C:on_money_change( money )
        print(money, "C receive event")
    end
    EventSystem:on(Event.MoneyChanged, C.on_money_change, {target = C})
    -- 在A模块中派发金币修改事件,当前金币为10
    EventSystem:emit(Event.MoneyChanged, 10)

    接下来会仔细解读一下这个EventSystem事件分发系统的Lua实现代码。

    实现事件分发系统时,需要小心一些特殊情况,比如有以下几个坑,读者可以留意一下代码中对这几个坑的处理

    为了便于讲解,下面的代码省略了一些非关键性的代码,用--- ...代替。

    五、注册监听事件接口

    function EventSystem:on( event, func, params )
        --- ...
        local event_listener = self._listeners[event]
        params = params or {}
        local priority = params.priority or 0
        local target = params.target
        --- ...
        local cb = {target = target, func = func, id = id, priority = priority}
        table.insert(event_listener.list, cb)
        id = id + 1
        if priority > 0 then
            event_listener.need_sort = true
            self:sort(event_listener)
        end
    end

    on方法中event参数表示要注册监听的事件名称,func参数表示当事件发生时要触发的回调函数,params表示额外参数,可以设置注册监听的目标target(可以利用它反注册所有与其相关的监听),也可以设置要注册监听的优先级,优先级越高的越先执行。

    on方法的实现还是比较简单的,主要就是将注册的相关信息插入到event_listener表中,但是明明注册的监听是有优先级的,却仍然只是调用table.insert将信息插入到表的末尾,这是为什么呢?读者可以先留意一下,后面会有详细解释。
    还需要格外注意的是sort方法

    function EventSystem:sort( listener )
        if listener.need_sort == true and listener.emit_count == 0 then
            table.sort(listener.list, function ( a, b )
                if a.priority == b.priority then
                    return a.id  b.id
                else
                    return a.priority > b.priority
                end
            end)
            listener.need_sort = false;
        end
    end

    可以看到sort方法必须在listener.emit_count == 0时才会进行排序,listener.emit_count == 0表示的是当前的事件没有处于派发状态,后面讲到派发接口时会详细解释,这里读者只需要知道其表示的含义即可。

    事件处于派发状态时不能进行优先级排序原因是可能会造成回调的重复触发。

    比如当前事件有4个回调 a, b, c, d,派发事件是顺序执行回调,当执行到第3个回调c时,如果在c回调中又注册了一个优先级最高的回调e,立刻排序的话,e插入到第一位,c会被挤到第4位,顺序执行到第4个回调时,导致c又被调用一次。

    六、反注册事件监听接口

    function EventSystem:off( event, func, params )
        --- ...
        local event_listener = self._listeners[event]
        params = params or {}
        for i,cb in ipairs(event_listener.list) do
            if cb.func == func and cb.target == params.target then
                if event_listener.emit_count > 0 then
                    -- 派发过程中只进行标记删除
                    cb.need_remove = true
                    event_listener.need_clean = true
                else
                    table.remove(event_listener.list, i)
                end
                break;
            end
        end
    end

    off方法用于取消事件监听,当事件未处于派发过程中时,直接调用table.remove移除注册信息即可,但当事件处于派发过程中时,不能直接移除,只能先进行标记。
    在事件处于派发过程中时不能直接移除的原因是可能导致遗漏触发某些回调,比如当前事件有5个回调 a, b, c, d, e,顺序执行到第3个回调c时,如果在c回调中调用了off方法取消自己的监听,此时直接移除c的话,会导致d回调移动到第3位,e移动到第4位,顺序执行到第4个回调时,调用的是e而遗漏了d。

    七、事件派发接口

    function EventSystem:emit( event, ... )
        --- ...
        local event_listener = self._listeners[event]
        local interrupt = false
        local length = #event_listener.list
        -- 这里不能使用ipairs,确保不会触发在派发过程中注册的事件
        -- 只取当前已经注册的事件数量,如果在派发过程中再注册(调用了table.insert),本次派发也不会调用
        for i = 1, length do
            if interrupt == true then
                break
            end
            local cb = event_listener.list[i]
            if cb.func and cb.need_remove ~= true then
                event_listener.emit_count = event_listener.emit_count + 1
                if cb.target then
                    interrupt = cb.func(cb.target, ...)
                else
                    interrupt = cb.func(...)
                end
                event_listener.emit_count = event_listener.emit_count - 1
            end
        end
        self:sort(event_listener);
        self:clean(event_listener);
        return interrupt
    end

    emit方法负责派发一个事件,顺序执行event_listener中注册的回调。事件的派发支持中断,当执行某个回调时,如果这个回调返回了true则可以中断当前事件的派发。

    值得一提的是,代码通过对应的event_listener.emit_count = event_listener.emit_count + 1event_listener.emit_count = event_listener.emit_count - 1来记录事件的派发状态,当emit_count > 0则表明事件还在派发过程中。当emit_count == 0则表明事件派发完成。

    不能使用event_listener.is_emiting = trueevent_listener.is_emiting = false代替的原因是如果在触发的回调中又派发了事件,形成了递归,那么二次派发事件结束时会直接将event_listener.is_emiting置为flase,导致一次派发事件对应的派发状态被标记错误

    八、更多

    事件分发系统的完整源码可以点击这里查看,测试用例可以点击这里查看
    更多Lua相关的设计与使用,比如面向对象(代码中用到的class关键字),组件系统,分模块加载等等,可以查看GitHub仓库LuaKit

    以上就是分析Lua观察者模式最佳实践之构建事件分发系统的详细内容,更多关于Lua 观察者模式 构建事件分发系统的资料请关注脚本之家其它相关文章!

    您可能感兴趣的文章:
    • SpringBoot+Redis执行lua脚本的方法步骤
    • 如何使用Vim搭建Lua开发环境详解
    • Lua中三种循环语句的使用讲解
    • Lua中的变量与赋值方法
    • Android事件分发机制(上) ViewGroup的事件分发
    • 详解EventDispatcher事件分发组件
    • Android View 事件分发机制详解
    • PHP中常用的三种设计模式详解【单例模式、工厂模式、观察者模式】
    • 浅谈发布订阅模式与观察者模式
    上一篇:如何使用Vim搭建Lua开发环境详解
    下一篇:详解Lua中的元表概念
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    分析Lua观察者模式最佳实践之构建事件分发系统 分析,Lua,观察者,模式,最佳,