Events
註冊
- 框架如何在啟動的時候載入註冊的事件?
- 框架如何觸發事件?
1,先在容器中註冊 events
的全域性物件。
Application
建構函式中對events
進行註冊程式碼
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
複製程式碼
展開
$this->register(new EventServiceProvider($this));
這裡的 $this->register()
方法就是呼叫 EventServiceProvider
物件的 register()
方法,最終在容器中對應的 events
物件
class EventServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
}
}
複製程式碼
2,註冊使用者定義的事件
這裡的部分涉及到
Provider
啟動 相關的流程,第八章的時候有講,我們直接跳到如何啟動EventServiceProvider
這裡。
先看
app.providers
中配置要載入的服務提供者。
'providers' => [
...
App\Providers\EventServiceProvider::class,
...
],
複製程式碼
App\Providers\EventServiceProvider::class
這裡的boot()
方法是被框架載入服務提供者的時候呼叫的。
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
public function boot()
{
parent::boot();
}
}
複製程式碼
先啟動父類的
boot()
方法
class EventServiceProvider extends ServiceProvider
{
protected $listen = [];
protected $subscribe = [];
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);
}
}
public function register()
{
//
}
public function listens()
{
return $this->listen;
}
}
複製程式碼
上面程式碼就是我們註冊的核心了,首先先遍歷 $listen
這個物件
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
複製程式碼
這段程式碼就是繫結事件的核心
Event::listen($event, $listener);
複製程式碼
這裡我們來看
Event
門面返回的是什麼
class Event extends Facade
{
...
protected static function getFacadeAccessor()
{
return 'events';
}
}
複製程式碼
實際上面回到了最初的地方,
events
是最初繫結的閉包
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
複製程式碼
events
就是Illuminate\Events\Dispatcher
這個類! 我們來看看Dispatcher
的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);
}
}
}
複製程式碼
這裡的程式碼其實就是賦值的過程
如果是全域性的事件就放入
$this->wildcards
中否則就放入 $this->listeners
中。
繼續看
$this->makeListener($listener);
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));
};
}
複製程式碼
這裡解析出來的閉包會賦值給 $this->listeners[$event]
。
到這裡我們就已經解析完了框架是如何對已經寫好的事件進行註冊
的。
3,訂閱者的註冊
事件除了一對一的繫結,還實現了一對多的繫結就是訂閱者
public function boot()
{
...
foreach ($this->subscribe as $subscriber) {
Event::subscribe($subscriber);
}
}
複製程式碼
回到 boot()
的方法中,執行完常規的 Event
的註冊,之後開始註冊 subscribe
到 Dispatcher
物件中。
public function subscribe($subscriber)
{
// "從容器中解析傳入的抽象,返回對應例項"
$subscriber = $this->resolveSubscriber($subscriber);
// "呼叫返回例項的subscribe($this)方法,同時傳入 $dispatcher 物件"
$subscriber->subscribe($this);
}
複製程式碼
那麼 subscribe($this)
裡面都做了什麼呢?
這裡我列舉了一個demo,寫法也是參照官方給出的。
public function subscribe($events)
{
$events->listen(
'App\Events\MyEvents',
'App\Listeners\MySubscribe@funName()'
);
}
複製程式碼
最後還是呼叫了 listen
方法,只不過這種方式可以支援一個訂閱者監聽多個事件,根據事件的不同選擇性的觸發對應的方法。
fire events !
前面都是講怎麼把事件繫結到 $dispatcher
物件中,這節我們開始講怎麼觸發事件!
事件呼叫的核心方法。
public function dispatch($event, $payload = [], $halt = false)
{
[$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);
if ($halt && ! is_null($response)) {
return $response;
}
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}
複製程式碼
我們來看個系統的預設的事件返回值
[$event, $payload] = $this->parseEventAndPayload(
$event, $payload
);
複製程式碼
這段程式碼是事件廣播的觸發
if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}
複製程式碼
開始遍歷從給定事件解析出來的監聽器類
foreach ($this->getListeners($event) as $listener)
{
......
}
複製程式碼
我們直接看 $this->getListeners($event);
方法
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;
}
複製程式碼
這裡的主要邏輯就是從之前的 $this->listeners
中找是否存在繫結的類
如果存在則返回對應的類,否則返回對應的實現介面。
最後執行
$response = $listener($event, $payload);
這裡的 $listener
就是在上面繫結的時候呼叫 makeListerer()
返回的閉包。
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));
};
}
複製程式碼
這裡傳入的引數 $event
是我們編寫的 Event
類的物件
$payload
是你要傳入攜帶的引數。
這裡的閉包邏輯後續詳解。
結語
Events
提供瞭解耦開發的優點,框架很多地方都有使用
比如模型的觀察器就是基於事件系統來實現的。