试想这样一个问题,当某个事件发生时,好比在游戏中A模块修改了用户的金币数,而B模块和C模块提供的功能都依赖于用户的金币数,那么,A模块在修改金币数的同时,就须要通知B模块和C模块。常规的方法就是A模块持有B模块和C模块的对象,而后分别经过调用对象接口的方式告诉它们,“嘿,我修改了用户的金币数,改为了10金币”。
但这样就带来了许多问题:git
为了解决上面的问题,咱们天然想到了观察者模式。github
这里简单说一下什么是观察者模式:定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的全部依赖者(称之为观察者)都会接收到通知并自动更新。
观察者模式的好处是,对象之间是松耦合的,当一个对象改变状态时,它并不须要知道本身的观察者是谁,只须要发布通知便可。任什么时候候均可以增长或删除观察者,不会影响到发布通知的对象。
而事件分发系统就是观察者模式的一个具体实现函数
事件分发系统核心须要提供的功能主要包括如下几个部分:测试
首先,来看看使用事件分发系统处理上面提到的问题,会是什么样的效果。
A模块只须要派发金币修改事件,B,C模块只须要订阅金币修改事件,以后即可以收到通知了。是否是很简单呢lua
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实现代码。
实现事件分发系统时,须要当心一些特殊状况,好比有如下几个坑,读者能够留意一下代码中对这几个坑的处理设计
为了便于讲解,下面的代码省略了一些非关键性的代码,用--- ...
代替。code
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 + 1
和event_listener.emit_count = event_listener.emit_count - 1
来记录事件的派发状态,当emit_count > 0
则代表事件还在派发过程当中。当emit_count == 0
则代表事件派发完成。
不能使用event_listener.is_emiting = true
和event_listener.is_emiting = false
代替的缘由是若是在触发的回调中又派发了事件,造成了递归,那么二次派发事件结束时会直接将event_listener.is_emiting
置为flase
,致使一次派发事件对应的派发状态被标记错误
事件分发系统的完整源码能够点击这里查看,测试用例能够点击这里查看
更多Lua相关的设计与使用,好比面向对象(代码中用到的class关键字),组件系统,分模块加载等等,能够查看GitHub仓库LuaKit