首先需要明确的几个问题:
Q1、什么是事件?
A:事件就是一个有名字的行为。当这个行为发生的时候,称这个事件被触发。
Q2、监听器又是什么?
A:监听器决定了事件的逻辑表达,由事件触发。监听器和事件往往是成对的,当然也可以是一个事件对应多个监听器。监听器是对事件的反应。当事件被触发时,由监听器做出反应。这样一来,多个事件的触发可以导致一个监听器做出反应。一个事件也可以有多个监听器做出反应。(一句话:监听器和事件之间的关系既可以是一对多,也可以是多对一)
Q3、事件管理器又是干嘛的?
A:事件管理器(EventManager),从名字上就可以看出来是管理事件用的。但他怎么管理呢?事件管理器往往会为多个事件聚合多个监听器(这里的事件和监听器都是不定数【就是可以是一个也可以是多个】)。事件管理器还负责触发事件。
一般来说我们用对象来表示事件。一个事件对象描述了事件的基本元素,包括何时以及如何触发这个事件。
关于事件的基本元素:事件名称、target(触发事件的对象,一般是事件对象本身)、事件参数。之前我们讲过事件相当与一个行为,在程序里面我们经常使用方法或函数来表示行为。因此事件的参数往往也是函数的参数。
另外关于Shared managers: 之前讲过一个事件可以针对多个监听器。这就是通过Shared managers实现的。EventManager的实现包含(组合)了SharedEventManagerInterface【在构造函数或者setSharedManager里面使用了代码注入的方式,详情可以查看源码】),而SharedEventManagerInterface描述了一个聚合监听器的对象,这些监听器只连接到拥有指定识别符的事件。SharedEventManager并不会触发事件,他只提供监听器并连接到事件。EventManger通过查询SharedEventMangaer来获取具有特定标识符的监听器。
EventManager里面几个重要的行为:
1、创建事件:创建事件实际上只是创建EventManagerInterface的一个实例
2、触发事件:一般在事件行为里面使用trigger触发,这样我们执行该行为的时候便可以直接触发该事件。函数原型:trigger($eventName,$target=null,$argv=[]);$eventName一般为时间行为名(常用__FUNCTION__代替),$target则为事件对象本身可用$this代替,$argv为传入事件的参数(一般为事件行为的参数)。
当然事件触发方式不仅仅只有trigger一种,还有triggerUntil,triggerEvent,triggerEventUntil。从名字上我们就可以看出分两类:trigger和triggerEvent;trigger类只单纯的触发事件,不需要实现创建事件实例只需要一个事件名字就可以了,而trigger不仅触发事件还顺带着触发监听器,需要事件实例。而带有Until后缀的方法都需要一个回调函数,每一个监听器的结果都会传到该回调函数中,如果回调函数返回了一个true的bool值,EventManager必须使监听器短路。(关于短路见下文的短回路)
更多内容请查看官方API,或者EventMangerInterface的具体注释。
3、创建监听器并连接到事件:
监听器可以通过EventManager创建,也可以通过SharedEventManager创建。两者都是使用attach方法,但参数有点儿不一样。
我们先看EventManager的方式:
方法原型:attach($eventName, callable $listener, $priority = 1)
很简单,我们只需要事件名,还有一个可调用函数,最后是优先级默认为1(zend里面的自带事件的优先级多为负数,所以如果你想让自定义的监听器优先级比较高,直接赋值一个正数就行了。)
可调用函数也就是我们的监听器。事件名有个特殊情况:“*”。这类似于正则匹配,将所有的事件都连接到本监听器中。
我们现在看看SharedEventManager方式:
方法原型:attach($identifier, $eventName, callable $listener, $priority = 1);
与之前唯一不同的地方多了个identifier参数。关于identifier的源码注释如下:
used to pull shared signals from SharedEventManagerInterface instance;
用来从SharedEventmanager实例中拉取分享信号。identifier是一个数组,按照我的理解:如果一个事件(注意SharedEventmanager无法创建事件的)定义了identifier,就意味着该事件是可共享的。让后SharedEventManger实例使用attach创建监听器的时候传入identifier参数。EventManager就可以使用identifier参数查询所有的监听器。
令人困惑的是既然有了事件名,那就可以通过事件名来查询相关监听器,那为何还要多此一举的添加identifier属性?我考虑到的是事件继承问题:假设有一个事件类Foo包含一个事件行为act,SubFoo继承了Foo类并重写了里面的事件行为act。两个类都的事件行为都具有相同的事件名act。这时候如果通过事件名来查询监听器,显然会有冲突。这时候我们定义identifier[__CLASS__, get_class($this)],并在监听器中指定identifier为SubFoo,显然会匹配到SubFoo类中的事件行为act。
以上我们通过SharedEventManager可以监听多个事件,另外我们还可以通过listener aggregates实现。通过Zend\EventManager\ListenerAggregateInterface,让一个类监听多个事件,连接一个或多个实例方法作为监听器。同样的该接口也定义了attach(EventManagerInterface $events)和detach(EventManagerInterface $events)。我们在attach的具体实现中使用EventManager的实例的方法attach监听到多个事件。
<span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\EventInterface; </span><span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\EventManagerInterface; </span><span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\ListenerAggregateInterface; </span><span style="color: #0000ff;">use</span> Zend\<span style="color: #008080;">Log</span><span style="color: #000000;">\Logger; </span><span style="color: #0000ff;">class</span> LogEvents <span style="color: #0000ff;">implements</span><span style="color: #000000;"> ListenerAggregateInterface { </span><span style="color: #0000ff;">private</span> <span style="color: #800080;">$listeners</span> =<span style="color: #000000;"> []; </span><span style="color: #0000ff;">private</span> <span style="color: #800080;">$log</span><span style="color: #000000;">; </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> __construct(Logger <span style="color: #800080;">$log</span><span style="color: #000000;">) { </span><span style="color: #800080;">$this</span>-><span style="color: #008080;">log</span> = <span style="color: #800080;">$log</span><span style="color: #000000;">; } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> attach(EventManagerInterface <span style="color: #800080;">$events</span><span style="color: #000000;">) { </span><span style="color: #800080;">$this</span>->listeners[] = <span style="color: #800080;">$events</span>->attach('do', [<span style="color: #800080;">$this</span>, 'log'<span style="color: #000000;">]); </span><span style="color: #800080;">$this</span>->listeners[] = <span style="color: #800080;">$events</span>->attach('doSomethingElse', [<span style="color: #800080;">$this</span>, 'log'<span style="color: #000000;">]); } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> detach(EventCollection <span style="color: #800080;">$events</span><span style="color: #000000;">) { </span><span style="color: #0000ff;">foreach</span> (<span style="color: #800080;">$this</span>->listeners <span style="color: #0000ff;">as</span> <span style="color: #800080;">$index</span> => <span style="color: #800080;">$listener</span><span style="color: #000000;">) { </span><span style="color: #800080;">$events</span>->detach(<span style="color: #800080;">$listener</span><span style="color: #000000;">); </span><span style="color: #0000ff;">unset</span>(<span style="color: #800080;">$this</span>->listeners[<span style="color: #800080;">$index</span><span style="color: #000000;">]); } } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> <span style="color: #008080;">log</span>(EventInterface <span style="color: #800080;">$e</span><span style="color: #000000;">) { </span><span style="color: #800080;">$event</span> = <span style="color: #800080;">$e</span>-><span style="color: #000000;">getName(); </span><span style="color: #800080;">$params</span> = <span style="color: #800080;">$e</span>-><span style="color: #000000;">getParams(); </span><span style="color: #800080;">$this</span>-><span style="color: #008080;">log</span>->info(<span style="color: #008080;">sprintf</span>('%s: %s', <span style="color: #800080;">$event</span>, json_encode(<span style="color: #800080;">$params</span><span style="color: #000000;">))); } }</span>
使用Aggregate的好处:
1、允许你使用有状态的监听器
2、在单一的类中组合多个相近的监听器,并一次性连接他们
内省监听器返回的结果
我们有了监听器,但如何接收他返回的结果呢?EventManager默认实现会返回一个ResponseCollection的实例。这个类继承于PHP的SplStack。基本结构是一个栈,所以允许你反序遍历Responses。
ResponseCollection提供了有用的几个方法:
first(): 获取第一个结果
last(): 获取最后一个结果
contains($value): 查看是否栈里面是否包含某一个值,如果包含则返回true,否则false。
短回路监听器执行:
什么叫短回路呢?假设你要做一件事情,直到这件事有了结果,这是一个回路。如果你提前知道了这件事的结果(比如之前做过这件事),那你就没比要把这件事完完全全的做完,这时候你只需要执行一个短回路。
我们在添加EventManager的时候有一个缓存机制。在一个方法中触发一个事件,如果我们找到一个缓存结果就直接返回。如果找不到缓存结果,我们就将触发的事件缓存下来以备后用。实际上和计算机硬件里面的高速缓存一个道理。
EventManager组件提供两种处理的方式:1、triggerUntil();2、triggerEventUntil。这两个方法都接受一个回调函数作为第一个参数。如果回调函数返回true,那执行停止。
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> someExpensiveCall(<span style="color: #800080;">$criteria1</span>, <span style="color: #800080;">$criteria2</span><span style="color: #000000;">) { </span><span style="color: #800080;">$params</span> = <span style="color: #008080;">compact</span>('criteria1', 'criteria2'<span style="color: #000000;">); </span><span style="color: #800080;">$results</span> = <span style="color: #800080;">$this</span>->getEventManager()-><span style="color: #000000;">triggerUntil( </span><span style="color: #0000ff;">function</span>(<span style="color: #800080;">$r</span><span style="color: #000000;">){ </span><span style="color: #0000ff;">return</span> (<span style="color: #800080;">$r</span><span style="color: #000000;"> instanceof SomeResultClass); }</span>, <span style="color: #ff00ff;">__FUNCTION__</span>, <span style="color: #800080;">$this</span>, <span style="color: #800080;">$params</span><span style="color: #000000;"> ); </span><span style="color: #0000ff;">if</span>(<span style="color: #800080;">$results</span>-><span style="color: #000000;">stopped()) { </span><span style="color: #0000ff;">return</span> <span style="color: #800080;">$results</span>->last()'<span style="color: #000000;"> } }</span>
从上面范例中,我们知道,如果执行停止了很有可能是因为栈里面最后的结果满足我们的要求。这样一来,我们只要返回该结果,何必还要进行多余的计算呢?
处理在事件中停止执行,我们还可以在监听器中停止执行。理由是我们曾经接收过某一个事件,现在我们又接收到了相同事件,理所当然的使用之前的结果就好了。这种情况下,监听器调用stopPropagation(true),然后EventManager会直接返回而不会继续通知额外的监听器。
<span style="color: #800080;">$events</span>->attach('do', <span style="color: #0000ff;">function</span>(<span style="color: #800080;">$e</span><span style="color: #000000;">) { </span><span style="color: #800080;">$e</span>-><span style="color: #000000;">stopPropagation(); </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> SomeResultClass(); });</span>
当然,使用触发器范例可能会导致歧义,毕竟你并不知道最终的结果是否满足要求。
Keeping it in order.
偶尔你会关心监听器的执行顺序。我们通过监听器的优先级来控制执行顺序(上面说讲的短回路也会影响执行顺序)。每一个EventManager::attach()和SharedEventManager::attach()都会接受一个而外的参数:priority。默认情况下为1,我们可以省略该参数。如果你提供了该参数:高优先级执行的早,低优先级的可能会推迟执行。
自定义事件对象:
我们之前使用trigger()触发事件,在这同时我们也创建了事件。但trigger()的参数有限,我们只能指定事件的对象,参数,名称。实际上我们可以创建一个自定义事件,在Zendframework里面有个很重要的事件:MvcEvent。很显然MvcEvent便是一个自定义事件,该事件组合了application实例,路由器,路由匹配对象,请求和应答对象,视图模型还有结果。我们查看MvcEvent的源码会发现MvcEvent类实际上继承了Event类。同理我们的自定义事件对象也可以继承Event类或者继承MvcEvent。
<span style="color: #800080;">$event</span> = <span style="color: #0000ff;">new</span><span style="color: #000000;"> CustomEvent(); </span><span style="color: #800080;">$event</span>->setName('foo'<span style="color: #000000;">); </span><span style="color: #800080;">$event</span>->setTarget(<span style="color: #800080;">$this</span><span style="color: #000000;">); </span><span style="color: #800080;">$event</span>->setSomeKey(<span style="color: #800080;">$value</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">injected with event name and target:</span> <span style="color: #800080;">$events</span>->triggerEvent(<span style="color: #800080;">$event</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">Use triggerEventUntil() for criteria-based short-circuiting:</span> <span style="color: #800080;">$results</span> = <span style="color: #800080;">$events</span>->triggerEventUntil(<span style="color: #800080;">$callback</span>, <span style="color: #800080;">$event</span>);
上面的代码可以看到我们使用自定义事件类创建了一个事件对象,调用相关拦截器为事件对象设置属性。我们有了事件对象还是用trigger()触发事件吗?显然不是,我们使用triggerEvent($event)方法,该方法接收一个事件对象。而triggerEventUntil有一个回调函数,该回调函数作为是否进行短回路的依据。