[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`類對象大概如下:

# 監聽器執行
事件監聽器綁定到事件之后,框架在初始化過程中,將這些配置加載到`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;
}
```
運行過程見上面的注釋。最后的結果大概是這樣的:

在`UserLogin`和`UserLogout`事件標識下,分別添加了對應的監聽器,分別是事件訂閱類`app\subscribe\User`的`onUserLogin`和`onUserLogout`。
監聽器添加完成后,接著是等待事件觸發。`login`操作中,執行了`Event::trigger('UserLogin');`,這將執行`app\subscribe\User`的`onUserLogin`,其執行過程跟前面分析的執行事件監聽器是一樣的,結果輸出如下:
```
用戶登錄了
我知道用戶登錄了,因為我訂閱了
```
同理,訪問`User`控制器的`logout`操作,得到:
```
用戶退出了
我知道用戶退出了,因為我訂閱了
```
事件訂閱分析完畢。總結一下,事件訂閱就是一種「復合」的監聽器,可以同時監聽多個事件。從其實現過程來看,本質和事件監聽器是一樣的,個人認為,使用事件訂閱的好處是僅僅集中管理代碼,把對某個對象(被觀察者)的多個動作的監聽,都寫在一個事件訂閱類里面,因而就不用另外寫相應多個動作的監聽器類。
# 關于觀察者模式
事件的實現機制,實際上是使用觀察者模式實現的。觀察者模式的好處是實現類的松耦合,被觀察者不需要知道觀察者到底做了什么,只需要觸發事件就夠了;另外,觀察者的數量可以靈活地增加、減少,而不用修改被觀察者。深入理解觀察者模式,可以參考這篇:[學好事件,先學學觀察者模式](https://segmentfault.com/a/1190000009459014)