糙解 Laravel 事件的实现原理

事件

事件可以解耦系统程序代码,使系统模块分明。事件本质上是一对多的关系,即当某个事件发生,会触发一系列的系统更新操作。php 提供了SplSubject, SplObserver, SplObjectStorage 标准库接口用来实现事件功能。
Laravel 的事件提供了一个简单的观察者实现,允许你在应用中订阅和监听各种发生的事件。

说明

在开始阅读之前,最好对 laravel 的基础知识有些微了解

  1. 服务容器
  2. 服务提供者
  3. 队列

EventServiceProvider 服务提供者

在 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 核心类

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 的处理

laravel 对队列的使用

事件注册机制

通过上述分析,事件注册本质上就是调用 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 的事件也有向上冒泡功能
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!