[TOC] # 说明 前面分析完一个请求的简单生命周期,其中涵盖了依赖注入、中间件的分析,这些不在单独分析。接下来将分析框架的事件机制。 # 事件配置文件的载入 ## 准备工作 项目根目录命令行运行:`php think make:listen ShowAppInit`创建一个监听器,这将在`app\listener`目录下生成一个`ShowAppInit.php`文件(如果没有`listener`目录则创建之)。接着简单修改`ShowAppInit.php`文件的代码如下: ``` <?php namespace app\listener; class ShowAppInit { public function handle($event) { echo "App 初始化啦" .PHP_EOL; } } ``` 监听器创建完成后,将其添加到`app`目录下的`event.php`文件: ``` return [ 'bind' => [ ], 'listen' => [ 'AppInit' => [ 'app\listener\ShowAppInit' ], //添加在这里 'HttpRun' => [], 'HttpEnd' => [], 'LogLevel' => [], 'LogWrite' => [], ], 'subscribe' => [ ], ]; ``` 这样就绑定了一个监听器(观察者)到`AppInit`事件,一旦该事件被触发,监听器将开始工作——执行其`handle`方法下的代码。 ## 配置载入 上面绑定监听器后,系统是在哪里载入了这些配置呢? 顺着一个请求的生命周期:Http::run()→Http::runWithRequest()→Http::initialize()->App::initialize()→App::load(),发现在`load`方法有这样几行: ``` if (is_file($appPath . 'event.php')) { $this->loadEvent(include $appPath . 'event.php'); } ``` 就是在这个位置,执行`loadEvent`方法加载事件的配置——该方法代码如下: ``` public function loadEvent(array $event): void { if (isset($event['bind'])) { // 将事件标识到事件(操作,比如一个控制器操作)的映射合并到「Event」类「$bing」成员变量中 // 比如 'UserLogin' => 'app\event\UserLogin', $this->event->bind($event['bind']); } if (isset($event['listen'])) { // 合并所有观察者(监听者)到Event类的listener数组 // 其形式为实际事件(被观察者)到观察者的映射 $this->event->listenEvents($event['listen']); } if (isset($event['subscribe'])) { // 订阅,实际上是一个批量的监听 // 就像一个人他同时订阅天气预报、股市行情、小花上QQ了…… // 一个订阅器,里面可以实现多个事件的监听 // 比如,我在一个订阅器中,同时监听用户登录,用户退出等操作 $this->event->subscribe($event['subscribe']); } } ``` 最终得到的`Event`类对象大概如下: ![](https://img.kancloud.cn/48/57/4857660699f0b2fabccc98e4d2c277ce_397x256.PNG) # 监听器执行 事件监听器绑定到事件之后,框架在初始化过程中,将这些配置加载到`Event`类的对象(当然也可以在程序中手动绑定监听器),接下来就可以决定在何时触发事件。`AppInit`事件是在`App::initialize()`方法中触发的,其代码如下: ``` $this->event->trigger('AppInit'); ``` 接着,我们看看`trigger`方法是如何触发事件的(如何调用监听器的`handle`方法)——其代码如下: ``` public function trigger($event, $params = null, bool $once = false) { // A 如果设置了关闭事件,则直接返回,不再执行任何监听器 if (!$this->withEvent) { return; } // B // 如果是一个对象,解析出对象的类 if (is_object($event)) { //将对象实例作为传入参数 $params = $event; $event = get_class($event); } //根据事件标识解析出实际的事件 if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $result = []; // 解析出事件的监听者(可多个) $listeners = $this->listener[$event] ?? []; foreach ($listeners as $key => $listener) { // C // 执行监听器的操作 $result[$key] = $this->dispatch($listener, $params); // 如果返回false,或者没有返回值且 $once 为 true,直接中断,不再执行后面的监听器 if (false === $result[$key] || (!is_null($result[$key]) && $once)) { break; } } // 是否返回多个监听器的结果 // $once 为 false 则返回最后一个监听器的结果 return $once ? end($result) : $result; } ``` ## A 决定是否继续执行监听器 `trigger`方法首先通过`$this->withEvent`判断监听器是否要执行,如果为否,则直接终止该方法。 `withEvent`的值可以通过如下方法设定: * 配置文件中,通过设置`app.with_event`的值。该值在`Http::runWithRequest()`方法中读取进来: ``` $this->app->event->withEvent($this->app->config->get('app.with_event', true)); ``` 由此,我们可以在配置文件中全局开启或者关闭事件机制。 * 通过`Event::withEvent`方法设置。由此也可以得知,我们在执行完一个监听器之后,可以通过`Event::withEvent`方法设置后面的监听器是否还要执行。 ## B 事件标识解析 这里传给`trigger`方法的是一个字符串(事件标识)`AppInit`,通过`$event = $this->bind[$event]` 得到`$event`的值为`think\event\AppInit`,再将该值作为键,`$listeners = $this->listener[$event]`,从`listener` 数组中获取实际的监听器,这里将得到`$listeners`为`[app\listener\ShowAppInit]`。 ## C 执行监听器 这里主要看`dispatch`方法: ``` protected function dispatch($event, $params = null) { // 如果不是字符串,比如,一个闭包 if (!is_string($event)) { $call = $event; //一个类的静态方法 } elseif (strpos($event, '::')) { $call = $event; } else { $obj = $this->app->make($event); $call = [$obj, 'handle']; } return $this->app->invoke($call, [$params]); } ``` 不管是闭包、静态类方法,还是监听器的`handle`方法,都是通过`invoke`方法来执行,`invoke`方法实现如下: ``` public function invoke($callable, array $vars = [], bool $accessible = false) { // 如果$callable是闭包 if ($callable instanceof Closure) { return $this->invokeFunction($callable, $vars); } // $callable不是闭包的情况 return $this->invokeMethod($callable, $vars, $accessible); } ``` 最终通过PHP反射类来执行对应的方法。 ## D 是否中断和返回结果 从代码实现可以看出,如果一个监听器方法最终返回false,或者没有返回值且 $once 为 true,则不再执行后面的监听器。`trigger`方法是返回多个监4听器的执行结果还是最后一个,由最后一个参数`$once`决定,`$once`为`true`,只返回最后一个监听器执行结果,反之,返回所有结果组成的数组。 # 监听器参数传递以及事件类 注意到监听器的`handle`方法还可以接收一个参数,从上面的分析可知,`trigger`方法的第二个参数最终将传给`handle`方法。 那么,在什么情况下需要用到这个参数呢?举个例子,假如要监听一个用户登录,我们可以新建一个监听器,绑定事件标识,在`handle`方法中实现业务逻辑——例如,输出:「有用户登录啦」,然后在登录代码的后面`trigger`这个事件标识。但如果我们又要知道是谁登录的话,这时我们可以把用户名作为`trigger`的第二个参数传入,在监听器的`handle`方法可以这样使用:`echo $event . 用户登录啦`。 当然,这里的`$event`也可以是一个事件类对象。 # 订阅 这里的订阅,本质上一种「复合」的监听器,比如,小明同时要监听小花跑、跳、吃饭、睡觉,这时,就可以把小明要针对这些动作做出的反应都放在一个类里面,方便管理。 ## 举个例子以及分析 ### 准备工作 * 在`app`目录下的`event.php`文件中的`listen`键添加两个事件标识,如下所示: ``` 'listen' => [ 'UserLogin' => [], 'UserLogout' => [], ], ``` 如果没有这两个事件标识,订阅类的方法将无法被添加到`Event`类的`$listener` 数组,导致最后无法执行到订阅类的方法的。 * 创建一个订阅类 项目根目录下,命令行运行:`php think make:subscribe User`,会在`app/subscribe`目录下创建一个订阅类,在该类中添加以下方法(如代码所示): ``` class User { public function onUserLogin(){ echo '我知道用户登录了,因为我订阅了<br>'; } public function onUserLogout(){ echo '我知道用户退出了,因为我订阅了<br>'; } } ``` * 创建控制器 在`app/controller`目录下创建一个`User`控制器,添加代码如下: ``` class User { public function __construct(){ //添加一个订阅类 Event::subscribe(\app\subscribe\User::class); } public function login(){ echo "用户登录了<br> "; Event::trigger('UserLogin'); } public function logout(){ echo "用户退出了<br> "; Event::trigger('UserLogout'); } } ``` ### 分析 假如访问`User`控制器的`login`操作,调试运行代码到`Event::subscrible()`方法,该方法代码如下: ``` public function subscribe($subscriber) { if (!$this->withEvent) { return $this; } // 强制转换为数组 $subscribers = (array) $subscriber; foreach ($subscribers as $subscriber) { if (is_string($subscriber)) { //实例化事件订阅类 $subscriber = $this->app->make($subscriber); } // 如果该事件订阅类存在'subscribe'方法,执行该方法 if (method_exists($subscriber, 'subscribe')) { // 手动订阅 $subscriber->subscribe($this); } else { // 智能订阅 $this->observe($subscriber); } } return $this; } ``` 这里关键看`observe`方法: ``` public function observe($observer, string $prefix = '') { if (!$this->withEvent) { return $this; } if (is_string($observer)) { $observer = $this->app->make($observer); } $reflect = new ReflectionClass($observer); $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC); // 支持订阅类中的方法指定前缀 if (empty($prefix) && $reflect->hasProperty('eventPrefix')) { $reflectProperty = $reflect->getProperty('eventPrefix'); $reflectProperty->setAccessible(true); $prefix = $reflectProperty->getValue($observer); } foreach ($methods as $method) { $name = $method->getName(); if (0 === strpos($name, 'on')) { // 自动将订阅类的方法添加到监听器 $this->listen($prefix . substr($name, 2), [$observer, $name]); } } return $this; } ``` 运行过程见上面的注释。最后的结果大概是这样的: ![](https://img.kancloud.cn/eb/48/eb48b202bbc4eb3b83f3106dcf6b8b4d_458x400.PNG) 在`UserLogin`和`UserLogout`事件标识下,分别添加了对应的监听器,分别是事件订阅类`app\subscribe\User`的`onUserLogin`和`onUserLogout`。 监听器添加完成后,接着是等待事件触发。`login`操作中,执行了`Event::trigger('UserLogin');`,这将执行`app\subscribe\User`的`onUserLogin`,其执行过程跟前面分析的执行事件监听器是一样的,结果输出如下: ``` 用户登录了 我知道用户登录了,因为我订阅了 ``` 同理,访问`User`控制器的`logout`操作,得到: ``` 用户退出了 我知道用户退出了,因为我订阅了 ``` 事件订阅分析完毕。总结一下,事件订阅就是一种「复合」的监听器,可以同时监听多个事件。从其实现过程来看,本质和事件监听器是一样的,个人认为,使用事件订阅的好处是仅仅集中管理代码,把对某个对象(被观察者)的多个动作的监听,都写在一个事件订阅类里面,因而就不用另外写相应多个动作的监听器类。 # 关于观察者模式 事件的实现机制,实际上是使用观察者模式实现的。观察者模式的好处是实现类的松耦合,被观察者不需要知道观察者到底做了什么,只需要触发事件就够了;另外,观察者的数量可以灵活地增加、减少,而不用修改被观察者。深入理解观察者模式,可以参考这篇:[学好事件,先学学观察者模式](https://segmentfault.com/a/1190000009459014)