【Laravel-海賊王系列】第九章, Events 功能解析

Jijilin發表於2019-02-25

Events 註冊

  1. 框架如何在啟動的時候載入註冊的事件?
  1. 框架如何觸發事件?

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 這個類! 我們來看看 Dispatcherlisten 方法

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 的註冊,之後開始註冊 subscribeDispatcher 物件中。

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
        );
複製程式碼

【Laravel-海賊王系列】第九章, Events 功能解析

這段程式碼是事件廣播的觸發

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 提供瞭解耦開發的優點,框架很多地方都有使用

比如模型的觀察器就是基於事件系統來實現的。

相關文章