糙解 Laravel 事件的實現原理

lufeijun1234發表於2019-11-28

事件可以解耦系統程式程式碼,使系統模組分明。事件本質上是一對多的關係,即當某個事件發生,會觸發一系列的系統更新操作。php 提供了SplSubject, SplObserver, SplObjectStorage 標準庫介面用來實現事件功能。
Laravel 的事件提供了一個簡單的觀察者實現,允許你在應用中訂閱和監聽各種發生的事件。

在開始閱讀之前,最好對 laravel 的基礎知識有些微瞭解

  1. 服務容器
  2. 服務提供者
  3. 佇列

在 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 引數型別不同,分情況解析監聽器程式碼

  1. 當 $listener 為字串時,會通過 createClassListener 方法進一步解析處理
  2. 當 $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');
}
  1. 先對字元進行處理,laravel 預期的字串為 \mespace\XXclass@method ,parseClassCallable 會用 @ 符號擷取字串,獲得類名和方法名,方法名預設為 handle
  2. createClassCallable 方法會通過服務容器,解析出監聽器類例項
  3. 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;
}
  1. getWildcardListeners 方法會解析 wildcards 中的監聽事件,這一部分主要是帶萬用字元的事件名稱,並且解析完後會進行記憶體快取
  2. addInterfaceListeners 方法會向上發散,會找到事件類所有實現的介面類,並且進一步解析 listeners 中是否有對應介面類的監聽器函式,藉此實現了類似 JavaScript 中的事件冒泡原理
  3. 獲取到事件的所有監聽器函式之後,會按照順序依次呼叫,由引數 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]); // 如果是類的話,後邊引數會被類覆蓋,
  1. laravel 事件處理,每個地方都在依據是否帶有萬用字元進行分情況處理,帶萬用字元的話,會將事件名作為引數傳遞
  2. 如果觸發事件的是類例項,laravel 會解析出類名作為事件名稱
  3. laravel 的事件也有向上冒泡功能

相關文章