事件管理模块

从零开始编写一个PHP框架 系列的《事件管理模块》

项目地址:terse

前言

事件管理器在框架中早已屡见不鲜,也有不少人称之为“钩子”,它在组件和需要侦听类之间充当着接口角色,这里我们将要实现一个事件管理器。

需求分析

  • 绑定监听事件
  • 事件触发
  • 移除监听事件
  • 事件冒泡
  • 事件优先级

目录结构

既然是事件管理器,那肯定要分为多个部分,事件和管理。

.
├── Event.php       [事件类]
└── Manager.php     [管理类]

事件类

先看一段 jQuery 的事件绑定代码。

var deleteMessage = 'delete current row';

$('.button').on('click', '.delete', function(e){
    e.stopPropagation();
    console.log(deleteMessage)
});

从上面的代码,我们分析到几点:

1、我们需要知道监听的是什么事件,比如:click
2、我们需要知道监听的对象是什么,比如:.delete
3、我们需要知道监听事件后做什么,比如:function(e){...}
4、我们需要知道触发时需要的参数,比如:deleteMessage
5、我们需要知道是否需要冒泡触发,比如:e.stopPropagation()

分析完成,我们就可以确定事件类的基本属性和方法了。

class Event
{
    /**
     * 监听事件名称
     * 
     * @var [type]
     */
    protected $_name;

    /**
     * 监听的对象
     * 
     * @var [type]
     */
    protected $_source;

    /**
     * 参数
     * 
     * @var [type]
     */
    protected $_data;

    /**
     * 是否停止冒泡
     * 
     * @var boolean
     */
    protected $_stopped = false;

    /**
     * 构造函数
     * 
     * @param string  $name
     * @param mixed   $source
     * @param mixed   $data
     * @param boolean $stopped
     */
    function __construct(string $name, $source, $data = null, $stopped = false)
    {
        $this->_name = $name;
        $this->_source = $source;
        $this->_data = $data;
        $this->_stopped = (bool)$stopped;
    }

    /**
     * 设置监听事件的名称
     * 
     * @param string $name
     */
    public function setName(string $name) {
        $this->_name = $name;
        return $this;
    }

    /**
     * 获取监听事件的名称
     * 
     * @return string
     */
    public function getName() {
        return $this->_name;
    }

    /**
     * 设置监听的对象
     * 
     * @param mixed $source
     */
    public function setSource($source) {
        $this->_source = $source;
        return $this;
    }

    /**
     * 获取监听的对象
     * 
     * @return mixed
     */
    public function getSource() {
        return $this->_source;
    }

    /**
     * 设置参数
     * 
     * @param mixed $data
     */
    public function setData($data) {
        $this->_data = $data;
        return $this;
    }

    /**
     * 获取参数
     * 
     * @return mixed
     */
    public function getData() {
        return $this->_data;
    }

    /**
     * 停止冒泡
     * 
     * @return void
     */
    public function stop() {
        $this->_stopped = true;
    }

    /**
     * 是否停止冒泡
     * 
     * @return boolean
     */
    public function isStop() {
        return $this->_stopped;
    }
}

细心的小伙伴应该看出了一个问题,就是我们总结的时候有五点,然而实现的时候只实现了四点。这是为什么呢?

因为我们在绑定事件的时候,会加上触发后要做的事情。然而在触发的时候,我们只需要知道触发什么事件,对谁触发,需要的参数等。

所以,事件触发后要做的事情会放在管理器里保存,也就是在绑定事件的时候指定。

管理类

如我们一开始的需求分析,我们需要提供几个方法。

绑定监听事件 ---> attach()
移除监听事件 ---> detach()
触发事件    ---> trigger()
清空事件    ---> empty()

绑定监听事件

按照 事件类 最后的说明,我们在绑定事件的时候,需要指定三个参数,分别是:绑定类型触发后做的事情优先级

过程分析:

  • 事件如果不存在,需要初始化事件
  • 如果存在,则找到第一个优先级比自己低的事件(倒序排序)
    如果找到,则将当前事件插入到这个事件之前
    如果没有找到,则将当前事件插入到事件列表末尾
<?php
class Manager
{
    /**
     * 事件
     * 
     * @var array
     */
    protected $_events = [];

    /**
     * 绑定监听事件
     * 
     * @param  string  $name
     * @param  mixed   $handler
     * @param  integer $priority
     * @return void
     */
    public function attach(string $name, $handler, $priority = 100)
    {
        $events = $this->_events;

        $data = [
            'handler' => $handler,
            'priority' => $priority
        ];

        // 如果事件不存在,初始化赋值
        if (!isset($events[$name])) {
            $events[$name][] = $data;
            $this->_events = $events;
            return true;
        }

        // 需要插入的位置
        $insertKey = null;

        // 找到第一个优先级比自己低的事件的key
        foreach ($events[$name] as $key => $event) {
            if ($event['priority'] < $priority) {
                $insertKey = $key;
                break;
            }
        }

        // 如果没有找到,说明自己的优先级最低
        if (is_null($insertKey)) {
            $events[$name][] = $data;
        } else {
            // 找到则插入事件
            array_splice($events[$name], $insertKey, 0, [$data]);
        }

        $this->_events = $events;
    }

    ...
}

移除监听事件

移除事件有两种情况:

1、移除某个事件列表中的某一个事件
2、移除某个事件列表

基于以上两点,我们需要两个参数,事件列表名称和事件触发后的操作。

过程分析:

  • 没找到指定类型的事件列表,返回真,表明已经移除
  • 找到指定类型的事件列表
    没有指定需要移除的事件,直接移除此事件列表,返回真
    有指定需要移除的事件,找到该事件并移除(如果相同类型的事件列表中有多个相同的事件,这里只移除其中一个)

关于移除指定事件列表的指定事件上,有点问题😂,一般不建议使用

<?php
class Manager
{
    ...

    /**
     * 移除监听事件
     * 
     * @param  string $name
     * @param  mixed  $handler
     * @return true
     */
    public function detach(string $name, $handler = null)
    {
        $events = $this->_events;

        // 不存在则返回真,表明移除成功
        if (!isset($events[$name])) {
            return true;
        }

        // 如果不是移除事件列表中的某一个,而是移除某一类,则移除后返回真
        if (!$handler) {
            unset($this->_events[$name]);
            return true;
        }

        // 找到事件,并移除
        // 如果出现同一类事件列表中出现多个相同事件,表明逻辑本身有问题,这里不加考虑
        foreach ($events[$name] as $key => $event) {
            if ($event['handler'] == $handler) {
                unset($this->_events[$name][$key]);
                break;
            }
        }
        return true;
    }
}

清空监听事件

这是最简单的了。

<?php
class Manager
{
    ...

    /**
     * 清空监听事件
     * 
     * @return void
     */
    public function clear()
    {
        $this->_events = [];
    }
}

触发监听事件

麻烦来了...

触发监听事件,我们需要知道几点:

1、触发的是哪个事件(必须)
2、触发的事件需要什么参数(可选)
3、可能是针对某个类进行的触发,那么这个类是什么(可选,优先级最低)

过程分析:

1、事件列表是否存在,不存在则返回真
2、初始化事件信息
3、执行该列表下的每一个事件
4、每执行完一个事件,检测一下是否需要停止,如果停止,则返回
5、可能会出现执行 db.beforeQuery 的情况,那么 db.beforeQuery 事件列表执行完之后,执行 db 事件列表(人工冒泡)。

<?php
class Manager
{
    ...

    /**
     * 触发监听事件
     * 
     * @return [type] [description]
     */
    /**
     * 触发监听事件
     * 
     * @param  string  $name
     * @param  mixed   $source
     * @param  array   $data
     * @return boolean
     */
    public function trigger(string $name, array $data = [], $source = null)
    {
        $events = $this->_events;

        if (!isset($events[$name])) {
            return false;
        }

        $result = false;

        // 初始化事件
        $event = new Event($name, $source, $data);

        // 先执行当前绑定事件
        $result = $this->triggerEvents($events[$name], $event);

        if ($event->isStop()) {
            return $result;
        }

        // 再寻找上一级
        if (strpos($name, '.') < 1) {
            return $result;
        }

        // 拆分模块
        list($type, $eventName) = explode('.', $name);

        if (!isset($events[$type])) {
            return $result;
        }

        // 赋值新的模块
        $event->setName($eventName);

        // 触发事件
        return $this->triggerEvents($events[$type], $event);
    }
}

在触发事件的时候,我们将触发事件的过程给剥离出来了。

过程分析:

1、检测 handler 是否是对象,如果不是对象,则不执行
2、如果是函数,直接调用执行
3、如果是类,则判断 name 是否是类的某一个方法,如果是,则执行

<?php
class Manager
{
    ...

    /**
     * 触发监听事件
     * 
     * @param  array  $events
     * @param  Event  $eventObj
     * @return true
     */
    protected function triggerEvents(array $events, Event $eventObj)
    {
        foreach ($events as $event) {
            $handler = $event['handler'];

            // 不是对象那还弄什么
            if (!is_object($handler)) {
                continue;
            }

            // 如果是函数,则直接执行
            if ($handler instanceof \Closure) {
                call_user_func_array($handler, [$eventObj]);

                if ($eventObj->isStop()) {
                    break;
                }
                continue;
            }

            // 不是函数就是类了
            $method = $eventObj->getName();
            if (method_exists($handler, $method)) {
                call_user_func_array([$handler, $method], [$eventObj]);

                if ($eventObj->isStop()) {
                    break;
                }
            }
        }

        return true;
    }
}

到此为止,事件管理模块就告一段落了。

完整代码

事件定义类:

<?php
namespace Terse\Event;

/**
* Terse\Event\Event
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
class Event
{
    /**
     * 监听事件名称
     * 
     * @var [type]
     */
    protected $_name;

    /**
     * 监听的对象
     * 
     * @var [type]
     */
    protected $_source;

    /**
     * 参数
     * 
     * @var [type]
     */
    protected $_data;

    /**
     * 是否停止冒泡
     * 
     * @var boolean
     */
    protected $_stopped = false;

    /**
     * 构造函数
     * 
     * @param string  $name
     * @param mixed   $source
     * @param mixed   $data
     * @param boolean $stopped
     */
    function __construct(string $name, $source, $data = null, $stopped = false)
    {
        $this->_name = $name;
        $this->_source = $source;
        $this->_data = $data;
        $this->_stopped = (bool)$stopped;
    }

    /**
     * 设置监听事件的名称
     * 
     * @param string $name
     */
    public function setName(string $name) {
        $this->_name = $name;
        return $this;
    }

    /**
     * 获取监听事件的名称
     * 
     * @return string
     */
    public function getName() {
        return $this->_name;
    }

    /**
     * 设置监听的对象
     * 
     * @param mixed $source
     */
    public function setSource($source) {
        $this->_source = $source;
        return $this;
    }

    /**
     * 获取监听的对象
     * 
     * @return mixed
     */
    public function getSource() {
        return $this->_source;
    }

    /**
     * 设置参数
     * 
     * @param mixed $data
     */
    public function setData($data) {
        $this->_data = $data;
        return $this;
    }

    /**
     * 获取参数
     * 
     * @return mixed
     */
    public function getData() {
        return $this->_data;
    }

    /**
     * 停止冒泡
     * 
     * @return void
     */
    public function stop() {
        $this->_stopped = true;
    }

    /**
     * 是否停止冒泡
     * 
     * @return boolean
     */
    public function isStop() {
        return $this->_stopped;
    }
}

事件管理类:

<?php
namespace Terse\Event;

// 绑定单个监听事件 ---> attach()
// 批量绑定监听事件 ---> batchAttach()
// 触发事件        ---> trigger()
// 移除单个监听事件 ---> detach()
// 批量移除监听事件 ---> batchDetach()
// 清空事件        ---> empty()
/**
* Terse\Event\Manager
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
class Manager
{
    /**
     * 事件
     * 
     * @var array
     */
    protected $_events = [];

    public function getEvents()
    {
        return $this->_events;
    }

    /**
     * 绑定监听事件
     * 
     * @param  string  $name
     * @param  mixed   $handler
     * @param  integer $priority
     * @return void
     */
    public function attach(string $name, $handler, $priority = 100)
    {
        $events = $this->_events;

        $data = [
            'handler' => $handler,
            'priority' => $priority
        ];

        // 如果事件不存在,初始化赋值
        if (!isset($events[$name])) {
            $events[$name][] = $data;
            $this->_events = $events;
            return true;
        }

        // 需要插入的位置
        $insertKey = null;

        // 找到第一个优先级比自己低的事件的key
        foreach ($events[$name] as $key => $event) {
            if ($event['priority'] < $priority) {
                $insertKey = $key;
                break;
            }
        }

        // 如果没有找到,说明自己的优先级最低
        if (is_null($insertKey)) {
            $events[$name][] = $data;
        } else {
            // 找到则插入事件
            array_splice($events[$name], $insertKey, 0, [$data]);
        }

        $this->_events = $events;
    }

    /**
     * 移除监听事件
     * 
     * @param  string $name
     * @param  mixed  $handler
     * @return true
     */
    public function detach(string $name, $handler = null)
    {
        $events = $this->_events;

        // 不存在则返回真,表明移除成功
        if (!isset($events[$name])) {
            return true;
        }

        // 如果不是移除事件列表中的某一个,而是移除某一类,则移除后返回真
        if (!$handler) {
            unset($this->_events[$name]);
            return true;
        }

        // 找到事件,并移除
        // 如果出现同一类事件列表中出现多个相同事件,表明逻辑本身有问题,这里不加考虑
        foreach ($events[$name] as $key => $event) {
            if ($event['handler'] == $handler) {
                unset($this->_events[$name][$key]);
                break;
            }
        }
        return true;
    }

    /**
     * 清空监听事件
     * 
     * @return void
     */
    public function clear()
    {
        $this->_events = [];
    }

    /**
     * 触发监听事件
     * 
     * @return [type] [description]
     */
    /**
     * 触发监听事件
     * 
     * @param  string  $name
     * @param  mixed   $source
     * @param  array   $data
     * @return boolean
     */
    public function trigger(string $name, array $data = [], $source = null)
    {
        $events = $this->_events;

        if (!isset($events[$name])) {
            return false;
        }

        $result = false;

        // 初始化事件
        $event = new Event($name, $source, $data);

        // 先执行当前绑定事件
        $result = $this->triggerEvents($events[$name], $event);

        if ($event->isStop()) {
            return $result;
        }

        // 再寻找上一级
        if (strpos($name, '.') < 1) {
            return $result;
        }

        // 拆分模块
        list($type, $eventName) = explode('.', $name);

        if (!isset($events[$type])) {
            return $result;
        }

        // 赋值新的模块
        $event->setName($eventName);

        // 触发事件
        return $this->triggerEvents($events[$type], $event);
    }

    /**
     * 触发监听事件
     * 
     * @param  array  $events
     * @param  Event  $eventObj
     * @return true
     */
    protected function triggerEvents(array $events, Event $eventObj)
    {
        foreach ($events as $event) {
            $handler = $event['handler'];

            // 不是对象那还弄什么
            if (!is_object($handler)) {
                continue;
            }

            // 如果是函数,则直接执行
            if ($handler instanceof \Closure) {
                call_user_func_array($handler, [$eventObj]);

                if ($eventObj->isStop()) {
                    break;
                }
                continue;
            }

            // 不是函数就是类了
            $method = $eventObj->getName();
            if (method_exists($handler, $method)) {
                call_user_func_array([$handler, $method], [$eventObj]);

                if ($eventObj->isStop()) {
                    break;
                }
            }
        }

        return true;
    }
}

最后

解除绑定事件慎用,如果使用不得当,会造成解除失败的情况,而系统并不会报错。

下一篇《请求模块》

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,323评论 19 139
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 13,794评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,797评论 8 265
  • 有一句话:听说过很多道理,却依然过不好这一生。 有一种事实:明明知道沉迷其中,浪费时间精力,却始终停不下来。 对于...
    金色年华A赵阅读 2,548评论 0 0
  • iBeacon 介绍 iBeacon 是苹果公司在 iOS7上配备的新功能,可以让附近的手持电子设备检测到一个由一...
    一铭_阅读 19,200评论 5 12