Laravel 的事件提供了一個簡單的觀察者實現,可以訂閱和監聽應用中發生的各類事件。事件機制是一種很好的應用解耦方式,由於一個事件能夠擁有多個互不依賴的監聽器。laravel
中事件系統由兩部分構成,一個是事件的名稱,事件的名稱能夠是個字符串,例如 event.email
,也能夠是一個事件類,例如 App\Events\OrderShipped
;另外一個是事件的 監聽器listener
,能夠是一個閉包,還能夠是監聽類,例如 App\Listeners\SendShipmentNotification
。laravel
咱們仍是經過官方文檔裏給出的這個例子來向下分析事件系統的源碼實現,不過在應用註冊事件和監聽器以前,Laravel在應用啓動時會先註冊處理事件用的events
服務。git
Laravel應用在建立時註冊的基礎服務裏就有Event
服務github
namespace Illuminate\Foundation;
class Application extends Container implements ...
{
public function __construct($basePath = null)
{
...
$this->registerBaseServiceProviders();
...
}
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
}
複製代碼
其中的 EventServiceProvider
是 /Illuminate/Events/EventServiceProvider
編程
public function register()
{
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
}
複製代碼
Illuminate\Events\Dispatcher
就是 events
服務真正的實現類,而Event
門面時events
服務的靜態代理,事件系統相關的方法都是由Illuminate\Events\Dispatcher
來提供的。數組
咱們仍是經過官方文檔裏給出的這個例子來向下分析事件系統的源碼實現,註冊事件和監聽器有兩種方法,App\Providers\EventServiceProvider
有個 listen
數組包含全部的事件(鍵)以及事件對應的監聽器(值)來註冊全部的事件監聽器,能夠靈活地根據需求來添加事件。bash
/**
* 應用程序的事件監聽器映射。
*
* @var array
*/
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],
];
複製代碼
也能夠在 App\Providers\EventServiceProvider
類的 boot
方法中註冊基於事件的閉包。閉包
/**
* 註冊應用程序中的任何其餘事件。
*
* @return void
*/
public function boot()
{
parent::boot();
Event::listen('event.name', function ($foo, $bar) {
//
});
}
複製代碼
能夠看到\App\Providers\EventProvider
類的主要工做就是註冊應用中的事件,這個註冊類的主要做用是事件系統的啓動,這個類繼承自 \Illuminate\Foundation\Support\Providers\EventServiceProvide
。app
咱們在將服務提供器的時候說過,Laravel應用在註冊完全部的服務後會經過\Illuminate\Foundation\Bootstrap\BootProviders
調用全部Provider的boot
方法來啓動這些服務,因此Laravel應用中事件和監聽器的註冊就發生在 \Illuminate\Foundation\Support\Providers\EventServiceProvide
類的boot
方法中,咱們來看一下:框架
public function boot()
{
foreach ($this->listens() as $event => $listeners) {
foreach ($listeners as $listener) {
Event::listen($event, $listener);
}
}
foreach ($this->subscribe as $subscriber) {
Event::subscribe($subscriber);
}
}
複製代碼
能夠看到事件系統的啓動是經過events
服務的監聽和訂閱方法來建立事件與對應的監聽器還有系統裏的事件訂閱者。ide
namespace Illuminate\Events;
class Dispatcher implements DispatcherContract
{
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);
}
}
}
protected function setupWildcardListen($event, $listener)
{
$this->wildcards[$event][] = $this->makeListener($listener, true);
}
}
複製代碼
對於包含通配符的事件名,會被統一放入 wildcards
數組中,makeListener
是用來建立事件對應的listener
的:
class Dispatcher implements DispatcherContract
{
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);
} else {
return $listener(...array_values($payload));
}
};
}
}
複製代碼
建立listener
的時候,會判斷監聽對象是監聽類仍是閉包函數。
對於閉包監聽來講,makeListener
會再包裝一層返回一個閉包函數做爲事件的監聽者。
對於監聽類來講,會繼續經過 createClassListener
來建立監聽者
class Dispatcher implements DispatcherContract
{
public function createClassListener($listener, $wildcard = false)
{
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return call_user_func($this->createClassCallable($listener), $event, $payload);
} else {
return call_user_func_array(
$this->createClassCallable($listener), $payload
);
}
};
}
protected function createClassCallable($listener)
{
list($class, $method) = $this->parseClassCallable($listener);
if ($this->handlerShouldBeQueued($class)) {
//若是當前監聽類是隊列的話,會將任務推送給隊列
return $this->createQueuedHandlerCallable($class, $method);
} else {
return [$this->container->make($class), $method];
}
}
}
複製代碼
對於經過監聽類的字符串來建立監聽者也是返回的一個閉包,若是當前監聽類是要執行隊列任務的話,返回的閉包是在執行後會將任務推送給隊列,若是是普通監聽類返回的閉包中會將監聽對象make出來,執行對象的handle
方法。 因此監聽者返回閉包都是爲了包裝好事件註冊時的上下文,等待事件觸發的時候調用閉包來執行任務。
建立完listener後就會把它放到listener
數組中以對應的事件名稱爲鍵的數組裏,在listener
數組中一個事件名稱對應的數組裏能夠有多個listener
, 就像咱們以前講觀察者模式時Subject
類中的observers
數組同樣,只不過Laravel比那個複雜一些,它的listener
數組裏會記錄多個Subject
和對應觀察者
的對應關係。
能夠用事件名或者事件類的對象來觸發事件,觸發事件時用的是Event::fire(new OrdershipmentNotification)
, 一樣它也來自events
服務
public function fire($event, $payload = [], $halt = false)
{
return $this->dispatch($event, $payload, $halt);
}
public function dispatch($event, $payload = [], $halt = false)
{
//若是參數$event事件對象,那麼就將對象的類名做爲事件名稱,對象自己做爲攜帶數據的荷載經過`listener`方法
//的$payload參數的實參傳遞給listener
list($event, $payload) = $this->parseEventAndPayload(
$event, $payload
);
if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}
$responses = [];
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
//若是觸發事件時傳遞了halt參數,而且listener返回了值,那麼就不會再去調用事件剩下的listener
//不然就將返回值加入到返回值列表中,等全部listener執行完了一併返回
if ($halt && ! is_null($response)) {
return $response;
}
//若是一個listener返回了false, 那麼將不會再調用事件剩下的listener
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}
protected function parseEventAndPayload($event, $payload)
{
if (is_object($event)) {
list($payload, $event) = [[$event], get_class($event)];
}
return [$event, Arr::wrap($payload)];
}
//獲取事件名對應的全部listener
public function getListeners($eventName)
{
$listeners = isset($this->listeners[$eventName]) ? $this->listeners[$eventName] : [];
$listeners = array_merge(
$listeners, $this->getWildcardListeners($eventName)
);
return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
複製代碼
事件觸發後,會從以前註冊事件生成的listeners
中找到事件名稱對應的全部listener
閉包,而後調用這些閉包來執行監聽器中的任務,須要注意的是:
false
後那麼事件就不會往下繼續傳播給剩餘的listener了,不然全部listener的返回值會在全部listener執行日後做爲一個數組統一返回。false
那麼事件會當即中止向剩餘的listener傳播。Laravel的事件系統原理仍是跟以前講的觀察者模式同樣,不過框架的做者功力深厚,巧妙的結合應用了閉包來實現了事件系統,還有針對須要隊列處理的事件,應用事件在一些比較複雜的業務場景中能利用關注點分散原則有效地解耦應用中的代碼邏輯,固然也不是什麼狀況下都能適合應用事件來編寫代碼,我以前寫過一篇文章Laravel事件驅動編程來講明事件的應用場景,感興趣的能夠去看看。
本文已經收錄在系列文章Laravel核心代碼學習裏,歡迎訪問閱讀。