yii2框架源码分析系列(6)之事件

回顾

上一篇聊了下yii2的Application,本来这篇应该继续后面的url解析了,但是有些前置知识还是需要提前解释,所以今天来介绍下yii2中的事件Event

Event

事件是yii2中一个非常重要的特性,可以很好的实现代码解耦,同时也是一种流行的任务流程设计模式 ,我们在业务处理中,都会碰到针对某个触发点而执行一个或多个事件的情况,而某些事件又可以埋到多个触发点,实现代码复用

实现

yii2中事件的实现主要通过Component类和Event类来实现,Event类是所有事件的基类,其中囊括了事件所需的参数和方法,直接看代码

class Event extends BaseObject
{
    // 事件名
    public $name;
    // 事件发布者
    public $sender;
    // 是否终止后续事件的执行,默认不终止
    public $handled = false;
    // 事件相关数据
    public $data;

    // 全局记录已注册事件
    private static $_events = [];
    // 全局记录已注册通配符模式事件
    private static $_eventWildcards = [];

    // 绑定类级别事件handler
    public static function on($class, $name, $handler, $data = null, $append = true)
    {
        $class = ltrim($class, '\\');

        // 类名或者事件名中有通配符,走通配符模式
        if (strpos($class, '*') !== false || strpos($name, '*') !== false) {
            if ($append || empty(self::$_eventWildcards[$name][$class])) {
                // 尾部添加到_eventWildcards静态数组中
                self::$_eventWildcards[$name][$class][] = [$handler, $data];
            } else {
                // 头部添加到_eventWildcards静态数组中
                array_unshift(self::$_eventWildcards[$name][$class], [$handler, $data]);
            }
            return;
        }

        if ($append || empty(self::$_events[$name][$class])) {
            // 尾部添加到_events静态数组中
            self::$_events[$name][$class][] = [$handler, $data];
        } else {
            // 头部添加到_events静态数组中
            array_unshift(self::$_events[$name][$class], [$handler, $data]);
        }
    }

    // 解绑类级别事件handler
    public static function off($class, $name, $handler = null)
    {
        $class = ltrim($class, '\\');
        if (empty(self::$_events[$name][$class]) && empty(self::$_eventWildcards[$name][$class])) {
            // 本来就没有绑定,直接返回false
            return false;
        }
        // 解绑所有handler
        if ($handler === null) {
            // 完全匹配模式解绑
            unset(self::$_events[$name][$class]);
            // 通配符模式解绑
            unset(self::$_eventWildcards[$name][$class]);
            return true;
        }

        // 解绑指定handler,完全匹配模式
        if (isset(self::$_events[$name][$class])) {
            $removed = false;
            foreach (self::$_events[$name][$class] as $i => $event) {
                if ($event[0] === $handler) {
                    // 找到指定的handler并解绑
                    unset(self::$_events[$name][$class][$i]);
                    // 设置解绑标识
                    $removed = true;
                }
            }
            if ($removed) {
                // 重新索引
                // 因为是数字索引,unset会造成索引值跳跃
                self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
                return $removed;
            }
        }

        // 解绑指定handler,通配符匹配模式
        $removed = false;
        foreach (self::$_eventWildcards[$name][$class] as $i => $event) {
            if ($event[0] === $handler) {
                // 找到指定的handler并解绑
                unset(self::$_eventWildcards[$name][$class][$i]);
                // 设置解绑标识
                $removed = true;
            }
        }
        if ($removed) {
            // 重新索引
            self::$_eventWildcards[$name][$class] = array_values(self::$_eventWildcards[$name][$class]);
            // 解绑之后处理掉空的数组元素
            // 这么做主要是为了减少后续正则匹配的消耗
            if (empty(self::$_eventWildcards[$name][$class])) {
                unset(self::$_eventWildcards[$name][$class]);
                if (empty(self::$_eventWildcards[$name])) {
                    unset(self::$_eventWildcards[$name]);
                }
            }
        }

        return $removed;
    }

    // 解绑所有类级别事件handler
    public static function offAll()
    {
        // 直接全部置空
        self::$_events = [];
        self::$_eventWildcards = [];
    }

    // 判断是否
    public static function hasHandlers($class, $name)
    {
        if (empty(self::$_eventWildcards) && empty(self::$_events[$name])) {
            // 没绑定过,那绝对不存在了
            // 这里判断的是_eventWildcards而不是_eventWildcards[$name],主要是因为该数组是以通配符模式保存的,并不能直接定位到$name
            return false;
        }

        if (is_object($class)) {
            // 获取对象名
            $class = get_class($class);
        } else {
            $class = ltrim($class, '\\');
        }
        // 这里需要说明下
        // 子类继承父类会同时会拥有父类绑定的事件
        // 类实现接口也会拥有接口绑定的事件
        // 所以你要找某个类某个事件是否绑定了handler,也需要判断其继承的所有父类和实现的所有接口
        $classes = array_merge(
            [$class],
            class_parents($class, true),
            class_implements($class, true)
        );

        // 完全匹配模式下查找绑定
        foreach ($classes as $class) {
            if (!empty(self::$_events[$name][$class])) {
                return true;
            }
        }

        // 通配符匹配模式下查找绑定
        foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
            // 这里使用yii2的Helper类中的方法,用于匹配通配符模式,这里不赘述该方法,有兴趣可以自己查阅
            // 先找到匹配的事件名name
            if (!StringHelper::matchWildcard($nameWildcard, $name)) {
                continue;
            }
            // 再匹配类名class
            foreach ($classHandlers as $classWildcard => $handlers) {
                if (empty($handlers)) {
                    // 没有绑定handler,直接跳过
                    continue;
                }
                foreach ($classes as $class) {
                    // 循环匹配所有类名,父类名,接口名
                    // 这里使用了!,仔细看matchWildcard这个方法了,里面会把\*这样的格式转换成[*]来处理,这个是有问题的,具体什么问题,感兴趣的可以评论里交流,这里就不说了,尽量不要滥用通配符匹配模式
                    if (!StringHelper::matchWildcard($classWildcard, $class)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    // 触发类级别事件
    public static function trigger($class, $name, $event = null)
    {
        $wildcardEventHandlers = [];
        // 获取通配符模式下匹配name的所有handler
        foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
            if (!StringHelper::matchWildcard($nameWildcard, $name)) {
                continue;
            }
            $wildcardEventHandlers = array_merge($wildcardEventHandlers, $classHandlers);
        }

        // 没有对应的handler,无法执行,直接返回
        if (empty(self::$_events[$name]) && empty($wildcardEventHandlers)) {
            return;
        }

        // 这里注意到trigger方法的第三个参数event应该是一个event类实例,主要是通过event类中的private属性来统一规范传递给handler的参数
        if ($event === null) {
            // 没有就自己造一个,这里用了延迟绑定,可用于子类调用
            $event = new static();
        }
        // 一些初始化
        $event->handled = false;
        $event->name = $name;

        if (is_object($class)) {
            if ($event->sender === null) {
                // 如果传的是对象,并且sender=null,直接将对象赋值给sender
                $event->sender = $class;
            }
            $class = get_class($class);
        } else {
            $class = ltrim($class, '\\');
        }

        // 老规矩,组装本类名,父类名,接口名
        $classes = array_merge(
            [$class],
            class_parents($class, true),
            class_implements($class, true)
        );

        foreach ($classes as $class) {
            // 单次循环要执行的handler
            // 也就是按照类的层级,从子类到父类再到接口逐步执行对应的handler
            $eventHandlers = [];
            foreach ($wildcardEventHandlers as $classWildcard => $handlers) {
                if (StringHelper::matchWildcard($classWildcard, $class)) {
                    // 收集通配符模式下匹配到的handler
                    $eventHandlers = array_merge($eventHandlers, $handlers);
                    // 这里每次匹配到一次之后应该unset掉,因为可能会导致重复执行
                    // 这里需要注意下,因为父类和子类可能使用的同一个匹配模式,所以,父类和子类的handler可能是交叉执行的,并不是按照层级递增来调用
                    unset($wildcardEventHandlers[$classWildcard]);
                }
            }

            // 收集完全匹配模式下的handler
            if (!empty(self::$_events[$name][$class])) {
                // 这里就不用unset了,因为具有唯一性
                $eventHandlers = array_merge($eventHandlers, self::$_events[$name][$class]);
            }

            // 执行单次循环收集到的handler,并把组装的event实例作为参数传进去
            foreach ($eventHandlers as $handler) {
                // 初始化data,这个data就是on()方法在绑定的时候定义的
                $event->data = $handler[1];
                // 这里是真正执行的位置
                // 不用我说你也知道吧,on()传入的handler的格式得遵循call_user_func的规则
                call_user_func($handler[0], $event);
                if ($event->handled) {
                    // 执行终止
                    // 这里传的是对象event,我们都知道event作为参数传递是引用传递,所以任何一个handler都可以在执行过程中将event的handled置为true,终止后面的handler,这也是为什么要硬性规定传递event对象作为参数的理由吧
                    return;
                }
            }
        }
    }
}

上面的代码中旧版是没有通配符匹配模式的,在2.0.14中加入的,但是我觉得这个没有实现好,上面注释有说明,而且从允许handler执行终止来看,handler的执行顺序是很重要的,但是通配符模式中并没有把控好这个顺序,甚至在hasHandlers()方法里面都有明显的错误,Event的实现大概就是这样,它包含了通用的静态方法和静态属性,来执行和保存全局,同时也包含私有属性,来规范并传递参数

Component

Event类继承的是基类BaseObject,貌似看着跟框架主体没有一点联系啊,那是因为Event默认是类级别的事件,正确的使用方式是你自己对你的业务涉及一些事件类,这些类都继承自Event,每个自定义事件类有自己独特的属性,这样就可以区分不同种类的事件了,那框架主体怎么使用事件机制的呢,这里就需要看看Component类了,Component实现的是全局级别的事件,会贯穿整个生命周期

// Component中对应处理事件的几个方法

// 绑定全局事件handler
// 既然是全局的,就需要传class了咯
public function on($name, $handler, $data = null, $append = true)
    {
        // 这个先别管,行为范畴,后面会讲
        $this->ensureBehaviors();

        // 同样区分匹配模式
        // 大致行为跟Event类差不多,只是保存handler的数组里少了class这一维度,并且保存handler的数组不再是static,而是private,因为我们通过入口创建的Application实例会继承到Component,所以属于单个实例私有的数据
        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]);
        }
    }

    // 解除全局事件绑定
    public function off($name, $handler = null)
    {
        $this->ensureBehaviors();
        // 没有绑定过,解除个毛线
        if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
            return false;
        }
        // 未指定handler,全解除
        if ($handler === null) {
            unset($this->_events[$name], $this->_eventWildcards[$name]);
            return true;
        }

        $removed = false;
        // plain event names
        if (isset($this->_events[$name])) {
            // 指定handler,逻辑与Event类中的off一样
            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;
            }
        }

        // 通配符模式,逻辑与Event类中的off一样
        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;
    }

    // 触发执行事件绑定的handler
    public function trigger($name, Event $event = null)
    {
        $this->ensureBehaviors();

        $eventHandlers = [];
        // 没有类这一层级了,就不需要循环调用了,直接收集所有符合的handler
        foreach ($this->_eventWildcards as $wildcard => $handlers) {
            if (StringHelper::matchWildcard($wildcard, $name)) {
                $eventHandlers = array_merge($eventHandlers, $handlers);
            }
        }

        if (!empty($this->_events[$name])) {
            // 这里和Event类差不多
            // 除了在设置sender上面,这里把this赋值给sender,表示当前实例
            $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;
                }
            }
        }

        // 这里还会触发类级别的事件handler
        Event::trigger($this, $name, $event);
    }

上面就是Component中实现的全局事件机制,只跟当前Application实例绑定,而Application通过注册Yii::$app来实现全局可访问,这样整个生命周期都可以执行全局事件的绑定、解绑和触发执行机制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,473评论 1 11
  • 这些天,南方地区持续的升温,无论是空间还是朋友圈都被高温给刷屏了。见到一个人,闲聊的第一句话竟是“这天热得……”取...
    心中住了一棵树阅读 308评论 0 0
  • 评论《贾老师》 我写的《贾老师》小文,只是记叙了一个乡村老教师的一个简单的侧面,却收到了人们...
    邯郸赵金海阅读 363评论 0 0
  • 十五元宵闹人圆 古人云:“千门开锁万灯明,正月中旬动地京”。众所周知,正月十五是元宵节,又称上元节、元夜、春灯节等...
    IDO老徐阅读 1,694评论 1 17