事件,在常识中的理解,就是发生的一件事情。在Yii2.0中,其实也是这样的一个概念,代表程序运行过程中的某一个特殊的点,可以理解为时间点,也可以理解为逻辑上的点,比如某个值、某个函数的返回结果等。

为什么?


可能有的伙计会问,为什么要用事件?事件有什么好处?解释这件事情之前,我们先来思考一个场景问题,大部分网站都有用户注册的功能,在用户注册过程中,我们主要的业务逻辑是将用户信息完整的存储到用户库中,这样才代表用户注册成功。然而用户注册成功后,还要同步完成一些其他的业务逻辑,比如给用户发送短信。这样就涉及到一个问题,当附加的逻辑越多,用户注册模块的代码耦合也会越来越严重,理解就越复杂。

Yii2.0为了让代码的理解更加容易,结构更加合理,引入了事件这个概念。在上面的问题中,我们可以将用户信息写入数据库后的这个时间节点看成是一个事件,这样程序中就存在了一个事件。事件是人为设定的,程序运行过程中,你认为的任何一个有意义的时间节点,像数据插入前、后,都可以成为一个事件。当我们通过这些事件(不同的时间节点)去了解这个程序时,即便再复杂的东西也可以拆分成多个阶段来理解。这就像是我们去了解网络一样,会分为物理层、数据链路层、网络层等。

怎么用?


上面对事件的理解只是狭义的,通过事件能够更好的了解这个流程后,我们就会有更高一点的要求,希望能够介入到这个流程,并能够改变这个流程,来实现我们自己的目的。那怎么实现呢?通俗点说就是往事件上面附加一些动作,当时间发生后能够触发这些动作来实现我们的目的。专业点说,就是Yii2.0中的事件处理器和事件监听者。我们想要在某个特定的时间点做点什么,就事先在这个对应的事件上绑定事件处理器,当流程走到这一步时,相应的处理器就被执行,完成我们事先设定的目的。想象一下:软件的运行就是沿着自己设定的路线,走过一个又一个重要节点,同时触发这些节点事件,最终走到自己生命终点的一个过程。将事件理解为流程中的某个点,不仅可以帮我更好的认识程序,也能更好的帮助我们改造程序。

其实从原理上面来看,Yii2.0的事件思想与设计模式中的观察者模式有异曲同工之妙。只是实现上面有一些区别罢了~

如何实现?


首先,我们要知道,Yii2.0的事件是在Component类和Event类中实现的(命名空间 yii/base),如果想要使用事件,那么要继承Component类。我们来看看Component类中关于事件的方法~

当然,Component类中还有关于Behavior(行为)的代码,我们在另一篇文章中进行描述。

绑定事件处理器

  1. <?php
  2. /**
  3. * @var array the attached event handlers (event name => handlers)
  4. */
  5. private $_events = [];
  6. /**
  7. * @var array the event handlers attached for wildcard patterns (event name wildcard => handlers)
  8. * @since 2.0.14
  9. */
  10. private $_eventWildcards = [];
  11. /**
  12. * Attaches an event handler to an event.
  13. *
  14. * The event handler must be a valid PHP callback. The following are
  15. * some examples:
  16. *
  17. *
  • function ($event) { … } // anonymous function
  • [$object, ‘handleClick’] // $object->handleClick()
  • [‘Page’, ‘handleClick’] // Page::handleClick()
  • ‘handleClick’ // global function handleClick()
  • ``` *
  • The event handler must be defined with the following signature, *
  • ```
  • function ($event)
  • ``` *
  • where $event is an [[Event]] object which includes parameters associated with the event. *
  • Since 2.0.14 you can specify event name as a wildcard pattern: *
  • ```php
  • $component->on(‘event.group.*’, function ($event) {
  • Yii::trace($event->name . ‘ is triggered.’);
  • });
  • ``` *
  • @param string $name the event name
  • @param callable $handler the event handler
  • @param mixed $data the data to be passed to the event handler when the event is triggered.
  • When the event handler is invoked, this data can be accessed via [[Event::data]].
  • @param bool $append whether to append new event handler to the end of the existing
  • handler list. If false, the new handler will be inserted at the beginning of the existing
  • handler list.
  • @see off() */ public function on($name, $handler, $data = null, $append = true) { $this->ensureBehaviors();

    if (strpos($name, ‘*’) !== false) {

     if ($append || empty($this->_eventWildcards[$name])) {
         $this->_eventWildcards[$name][] = [$handler, $data];
     } else {
         array_unshift($this->_eventWildcards[$name], [$handler, $data]);
     }
     return;
    

    }

    if ($append || empty($this->_events[$name])) {

     $this->_events[$name][] = [$handler, $data];
    

    } else {

     array_unshift($this->_events[$name], [$handler, $data]);
    

    } } ``` 首先,在类中定义两个成员变量,$_event和$_eventWildcards,数组类型,用于存放事件相关的信息。
    on 函数,主要的作用是为事件绑定相应的事件处理器~
    参数的含义~

  • $name:事件名称
  • $handle:事件处理器名称(通常是一个函数名)
  • $data:事件处理器需要的参数(函数参数)
  • $append:事件处理器存放的顺序标志

$this->ensureBehaviors() 该行代码与行为有关,另一篇关于行为的文章中有解释。
整个代码的逻辑如下~

  1. 判断事件名称是否含有通配符 * ,如果含有,则判断 $append的值 和 该事件在$_eventWildcards数组中是否存在其他的事件处理器,两者进行或运算。
  2. $append的值默认是true,将该事件处理器放在事件处理程序列表的最后面,触发事件后 最后执行,为false则放在列表最前面。
  3. 如果该事件没有其他的事件处理器,则直接放入到$_eventWildcards数组中,键名是事件名称,内容是 事件处理器名称和传递的数据 组成的数组。
  4. 如果事件名称中没有通配符 * ,则将事件处理器放入到 $_event数组中,存放规则同步骤2和3。

主要的思想就是将事件处理器放入到一个以事件名为键名的数组中,以及确定事件处理器的存放顺序。

解绑事件处理器

<?php

/**
 * Detaches an existing event handler from this component.
 *
 * This method is the opposite of [[on()]].
 *
 * Note: in case wildcard pattern is passed for event name, only the handlers registered with this
 * wildcard will be removed, while handlers registered with plain names matching this wildcard will remain.
 *
 * @param string $name event name
 * @param callable $handler the event handler to be removed.
 * If it is null, all handlers attached to the named event will be removed.
 * @return bool if a handler is found and detached
 * @see on()
 */
public function off($name, $handler = null)
{
    $this->ensureBehaviors();
    if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
        return false;
    }
    if ($handler === null) {
        unset($this->_events[$name], $this->_eventWildcards[$name]);
        return true;
    }

    $removed = false;
    // plain event names
    if (isset($this->_events[$name])) {
        foreach ($this->_events[$name] as $i => $event) {
            if ($event[0] === $handler) {
                unset($this->_events[$name][$i]);
                $removed = true;
            }
        }
        if ($removed) {
            $this->_events[$name] = array_values($this->_events[$name]);
            return $removed;
        }
    }

    // wildcard event names
    if (isset($this->_eventWildcards[$name])) {
        foreach ($this->_eventWildcards[$name] as $i => $event) {
            if ($event[0] === $handler) {
                unset($this->_eventWildcards[$name][$i]);
                $removed = true;
            }
        }
        if ($removed) {
            $this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
            // remove empty wildcards to save future redundant regex checks:
            if (empty($this->_eventWildcards[$name])) {
                unset($this->_eventWildcards[$name]);
            }
        }
    }

    return $removed;
}

off 函数,主要作用是为事件解绑相应的事件处理器~
整段代码的逻辑如下~

  1. 首先确定该事件是否存在,判断数组$_events和$_eventWildcards中是否存在键名是指定事件名的内容。
  2. 如果存在该事件,但是未指定事件处理器的名称,则直接删除事件处理器列表,所有的事件处理器失效。如果指定了事件处理器名称,继续进行下面的判断。
  3. 设置删除标识为false,分别判断是否存在$_events和$_eventWildcards数组中。
  4. 存在于$_evnets数组中,则遍历该数组,在事件顺序列表中找到该事件处理器并删除,同时设置删除标识为true,最后重新索引事件顺序列表。
  5. 存在于$_eventWildcards数组中,处理逻辑同$_event。

主要思想就是在存放事件处理器的数组中找到对应的事件处理器并删除。

触发事件处理器

<?php

/**
 * Triggers an event.
 * This method represents the happening of an event. It invokes
 * all attached handlers for the event including class-level handlers.
 * @param string $name the event name
 * @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
 */
public function trigger($name, Event $event = null)
{
    $this->ensureBehaviors();

    $eventHandlers = [];
    foreach ($this->_eventWildcards as $wildcard => $handlers) {
        if (StringHelper::matchWildcard($wildcard, $name)) {
            $eventHandlers = array_merge($eventHandlers, $handlers);
        }
    }

    if (!empty($this->_events[$name])) {
        $eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
    }

    if (!empty($eventHandlers)) {
        if ($event === null) {
            $event = new Event();
        }
        if ($event->sender === null) {
            $event->sender = $this;
        }
        $event->handled = false;
        $event->name = $name;
        foreach ($eventHandlers as $handler) {
            $event->data = $handler[1];
            call_user_func($handler[0], $event);
            // stop further handling if the event is handled
            if ($event->handled) {
                return;
            }
        }
    }

    // invoke class-level attached handlers
    Event::trigger($this, $name, $event);
}

trigger 函数,主要作用是顺序调用事件顺序列表中的事件处理器~
参数的含义:

  • $name:事件名称
  • $event:Event类的实例或null

整段代码的逻辑如下~

  1. 首先遍历通配符相关的事件列表,找到所有与事件相匹配的事件处理器,放入到$eventHandlers集合中。
  2. 如果$_event事件集合列表中也存在事件处理器,则合并到$eventHandlers集合中。
  3. 如果$eventHandlers集合不为空,则进行调用前的数据封装。
  4. 如果$event为空,也就是没有传递这个参数,则实例化Event这个类,并设置实例的sender、handled、name、data属性。
  5. 循环遍历$eventHandlers集合中的事件处理器,并将$event实例最为参数传递给事件处理器。判断传递的$event实例中handled属性是否为true,如果是true,直接返回,事件顺序列表中的其他事件处理器不在进行回调。
  6. 调用类级别的事件。

小思考?
为什么还要传递一个$event实例的参数,有什么意义?
我的理解是为了向事件处理器传递参数时更具有扩展性。首先Event类中定义了成员变量data,在我们绑定事件处理器的过程中,还可以绑定一些数据,在触发时,进行参数的传递,我们也可以看到在trigger的实现代码中$event的data属性被赋值,但是当我们想要在触发的时候也向事件处理器传递一些参数时要怎么办呢?这时我们就可以借助Event类的子类的实例来进行参数的传递。有的同学可能会问,用一个数组也可以实现啊,确实,数组也可以实现,但是为什么用Event的实例而不用数组呢?我猜想是因为代码的可维护性,一个Event类的子类,让代码理解起来更加容易,传递的参数一目了然,有一些文档的意思。

关于类级别的事件,所有的实现全部在Event类中,具体的实现代码与上面大体相同,只是作用的范围不同,实例级别的事件只限于在当前实例中触发,不会影响到别的实例,而类中的事件,是类及其子类的所有实例都会触发。

代码应用


我们看一下例子~

<?php

class User extends Component 
{      
    const EVENT_REGISTER_AFTER = 'register_after';

      public function regiest()
    {
        ....用户注册逻辑

            return $uuid;      
    }
}

class UserEvent extends Event
{
        public $uuid;

    public $name;
}

class Msg
{
    public funtion send()
    {
        ....发送短息逻辑  
    }
}

class App 
{
    public function index() 
    {
        $name = 'li';
        $user = new User();

        // 绑定事件处理器
        $user->on(User::EVENT_REGISTER_AFTER, [Msg, send], [1, 2, 3]);

        $uuid = $user->regiest();

        // 触发事件处理器
        $user->trigger(User::EVENT_REGISTER_AFTER, new UserEvent(['uuid' => $uuid, 'name' => $name]));
    }
}

用户注册入口 App类的index方法。我们在User类中声明一个关于用户注册完成的事件,然后在程序开始时将事件和事件处理器绑定起来,注册完成后触发事件。