事件可以解耦系統程式程式碼,使系統模組分明。事件本質上是一對多的關係,即當某個事件發生,會觸發一系列的系統更新操作。php 提供了SplSubject, SplObserver, SplObjectStorage 標準庫介面用來實現事件功能。
Laravel 的事件提供了一個簡單的觀察者實現,允許你在應用中訂閱和監聽各種發生的事件。
在開始閱讀之前,最好對 laravel 的基礎知識有些微瞭解
- 服務容器
- 服務提供者
- 佇列
在 laravel 系統中,EventServiceProvider 負責提供事件的實現與排程,作為 laravel 核心服務提供者,在系統初始化函式中就被註冊,核心程式碼塊為
// vendor/laravel/framework/src/Illuminate/Events/EventServiceProvider.php
public function register()
{
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
}
說明:
- 核心是往 laravel 的服務容器中繫結事件介面和事件的實現類
- Dispatcher 類是事件實現的核心類。
- QueueFactoryContract 是標註對應的佇列實現類
為了簡明邏輯,將核心放到事件實現上,可以忽略佇列相關東西,可以將程式碼簡化為
public function register()
{
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app));
});
}
Dispatcher 類是 laravel 提供事件服務的核心程式碼,事件本質上就兩個核心函式
1、listen 方法,負責繫結事件名稱和事件監聽器程式碼的對應關係,事件名稱通過判斷是否包含 " * " 分為明確事件名稱和萬用字元事件名稱
2、dispatch 方法,負責排程監聽器程式碼,完成系統事件更新
listen 方法解析
public function listen($events, $listener)
{
foreach ((array) $events as $event) {
if (Str::contains($event, '*')) {
$this->setupWildcardListen($event, $listener);
} else {
$this->listeners[$event][] = $this->makeListener($listener);
}
}
}
laravel 將事件對映分別儲存到 wildcards 和 listeners 屬性中
public function makeListener($listener, $wildcard = false)
{
if (is_string($listener)) {
return $this->createClassListener($listener, $wildcard);
}
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return $listener($event, $payload);
}
return $listener(...array_values($payload));
};
}
在 makeListener 方法中,分依據 $listener 引數型別不同,分情況解析監聽器程式碼
- 當 $listener 為字串時,會通過 createClassListener 方法進一步解析處理
- 當 $listener 為閉包函式時,會進一步進行包裝,將事件名和引數作為閉包函式的引數,在閉包函式內,依據 $wildcard 直接呼叫對應的監聽器程式碼。這一步封裝主要為了在排程時方便統一處理
public function createClassListener($listener, $wildcard = false)
{
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return call_user_func($this->createClassCallable($listener), $event, $payload);
}
return call_user_func_array(
$this->createClassCallable($listener), $payload
);
};
}
protected function createClassCallable($listener)
{
[$class, $method] = $this->parseClassCallable($listener);
***省了佇列的一些處理****
return [$this->container->make($class), $method];
}
protected function parseClassCallable($listener)
{
return Str::parseCallback($listener, 'handle');
}
- 先對字元進行處理,laravel 預期的字串為 \mespace\XXclass@method ,parseClassCallable 會用 @ 符號擷取字串,獲得類名和方法名,方法名預設為 handle
- createClassCallable 方法會通過服務容器,解析出監聽器類例項
- createClassListener 方法也會進行閉包封裝,引數依然是事件名稱和引數,這一點和上述對閉包的封裝一樣
dispatch 方法解析
當對應事件觸發時,系統會通過 dispatch 方法進行排程,呼叫之前註冊過的監聽函式,完成事件更新任務。
public function dispatch($event, $payload = [], $halt = false)
{
[$event, $payload] = $this->parseEventAndPayload(
$event, $payload
);
**** 省略佇列處理程式碼 ****
$responses = [];
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
if ($halt && ! is_null($response)) {
return $response;
}
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}
這是事件排程的核心程式碼
protected function parseEventAndPayload($event, $payload)
{
if (is_object($event)) {
[$payload, $event] = [[$event], get_class($event)];
}
return [$event, Arr::wrap($payload)];
}
該方法主要是為了解析一下事件名和引數,$event 解析為字串,$payload 解析為陣列。當引數 $event 為物件時, laravel 會解析出類名作為事件名稱,並且將類例項作為 $payload 陣列引數返回
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];
$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);
return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
protected function getWildcardListeners($eventName)
{
$wildcards = [];
foreach ($this->wildcards as $key => $listeners) {
if (Str::is($key, $eventName)) {
$wildcards = array_merge($wildcards, $listeners);
}
}
return $this->wildcardsCache[$eventName] = $wildcards;
}
protected function addInterfaceListeners($eventName, array $listeners = [])
{
foreach (class_implements($eventName) as $interface) {
if (isset($this->listeners[$interface])) {
foreach ($this->listeners[$interface] as $names) {
$listeners = array_merge($listeners, (array) $names);
}
}
}
return $listeners;
}
- getWildcardListeners 方法會解析 wildcards 中的監聽事件,這一部分主要是帶萬用字元的事件名稱,並且解析完後會進行記憶體快取
- addInterfaceListeners 方法會向上發散,會找到事件類所有實現的介面類,並且進一步解析 listeners 中是否有對應介面類的監聽器函式,藉此實現了類似 JavaScript 中的事件冒泡原理
- 獲取到事件的所有監聽器函式之後,會按照順序依次呼叫,由引數 halt 或者 監聽器函式返回值( false ) 來決定是否停止繼續執行剩餘監聽器程式碼,所以,在繫結事件監聽器時,繫結的順序也是很重要的
Dispatcher 的其餘程式碼
開啟 Dispatcher 實現的介面類,發現還有一些其他方法,例如:push、flush、forget、hasListeners 等等,這些都是一些輔助的方法函式,都是對 listen / dispatch 的呼叫,或者是對 listeners / wildcards / wildcardsCache 的處理
事件序號產生器制
通過上述分析,事件註冊本質上就是呼叫 listener 函式,進行事件名和事件處理函式的關係繫結。
$event = $app->make("events");
// 繫結事件名稱 和 類字串
$event->listen('order',App\Listeners\OrderListenerOne::class);
$event->listen(App\Events\OrderEvent::class,App\Listeners\OrderListenerOne::class);
// 繫結事件名稱 和 閉包函式
$event->listen('order',function( $a , $b ){
echo "<hr>";
echo $a, "<br>";
echo $b, "<br>";
echo "<hr>";
});
事件觸發排程
// 字串名稱觸發
$event->dispatch("order",[1,11,22]);
// 類事件觸發
$one = new App\Events\OrderEvent(1);
$event->dispatch($one,[1,11,22]); // 如果是類的話,後邊引數會被類覆蓋,
- laravel 事件處理,每個地方都在依據是否帶有萬用字元進行分情況處理,帶萬用字元的話,會將事件名作為引數傳遞
- 如果觸發事件的是類例項,laravel 會解析出類名作為事件名稱
- laravel 的事件也有向上冒泡功能