laravel 糙解之事件

php   laravel  

事件

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

说明

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

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

EventServiceProvider 服务提供者

在 laravel 系统中,EventServiceProvider 负责提供事件的实现与调度,作为 laravel 核心服务提供者,在系统初始化函数中就被注册,核心代码块为

  1. // vendor/laravel/framework/src/Illuminate/Events/EventServiceProvider.php
  2. public function register()
  3. {
  4. $this->app->singleton('events', function ($app) {
  5. return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
  6. return $app->make(QueueFactoryContract::class);
  7. });
  8. });
  9. }

说明:

  • 核心是往 laravel 的服务容器中绑定事件接口和事件的实现类
  • Dispatcher 类是事件实现的核心类。
  • QueueFactoryContract 是标注对应的队列实现类

为了简明逻辑,将核心放到事件实现上,可以忽略队列相关东西,可以将代码简化为

  1. public function register()
  2. {
  3. $this->app->singleton('events', function ($app) {
  4. return (new Dispatcher($app));
  5. });
  6. }

Dispatcher 核心类

Dispatcher 类是 laravel 提供事件服务的核心代码,事件本质上就两个核心函数
1、listen 方法,负责绑定事件名称和事件监听器代码的对应关系,事件名称通过判断是否包含 “ * “ 分为明确事件名称和通配符事件名称
2、dispatch 方法,负责调度监听器代码,完成系统事件更新

listen 方法解析

  1. public function listen($events, $listener)
  2. {
  3. foreach ((array) $events as $event) {
  4. if (Str::contains($event, '*')) {
  5. $this->setupWildcardListen($event, $listener);
  6. } else {
  7. $this->listeners[$event][] = $this->makeListener($listener);
  8. }
  9. }
  10. }

laravel 将事件映射分别存储到 wildcards 和 listeners 属性中

  1. public function makeListener($listener, $wildcard = false)
  2. {
  3. if (is_string($listener)) {
  4. return $this->createClassListener($listener, $wildcard);
  5. }
  6. return function ($event, $payload) use ($listener, $wildcard) {
  7. if ($wildcard) {
  8. return $listener($event, $payload);
  9. }
  10. return $listener(...array_values($payload));
  11. };
  12. }

在 makeListener 方法中,分依据 $listener 参数类型不同,分情况解析监听器代码

  1. 当 $listener 为字符串时,会通过 createClassListener 方法进一步解析处理
  2. 当 $listener 为闭包函数时,会进一步进行包装,将事件名和参数作为闭包函数的参数,在闭包函数内,依据 $wildcard 直接调用对应的监听器代码。这一步封装主要为了在调度时方便统一处理
  1. public function createClassListener($listener, $wildcard = false)
  2. {
  3. return function ($event, $payload) use ($listener, $wildcard) {
  4. if ($wildcard) {
  5. return call_user_func($this->createClassCallable($listener), $event, $payload);
  6. }
  7. return call_user_func_array(
  8. $this->createClassCallable($listener), $payload
  9. );
  10. };
  11. }
  12. protected function createClassCallable($listener)
  13. {
  14. [$class, $method] = $this->parseClassCallable($listener);
  15. ***省了队列的一些处理****
  16. return [$this->container->make($class), $method];
  17. }
  18. protected function parseClassCallable($listener)
  19. {
  20. return Str::parseCallback($listener, 'handle');
  21. }
  1. 先对字符进行处理,laravel 预期的字符串为 \mespace\XXclass@method ,parseClassCallable 会用 @ 符号截取字符串,获得类名和方法名,方法名默认为 handle
  2. createClassCallable 方法会通过服务容器,解析出监听器类实例
  3. createClassListener 方法也会进行闭包封装,参数依然是事件名称和参数,这一点和上述对闭包的封装一样

dispatch 方法解析

当对应事件触发时,系统会通过 dispatch 方法进行调度,调用之前注册过的监听函数,完成事件更新任务。

  1. public function dispatch($event, $payload = [], $halt = false)
  2. {
  3. [$event, $payload] = $this->parseEventAndPayload(
  4. $event, $payload
  5. );
  6. **** 省略队列处理代码 ****
  7. $responses = [];
  8. foreach ($this->getListeners($event) as $listener) {
  9. $response = $listener($event, $payload);
  10. if ($halt && ! is_null($response)) {
  11. return $response;
  12. }
  13. if ($response === false) {
  14. break;
  15. }
  16. $responses[] = $response;
  17. }
  18. return $halt ? null : $responses;
  19. }

这是事件调度的核心代码

  1. protected function parseEventAndPayload($event, $payload)
  2. {
  3. if (is_object($event)) {
  4. [$payload, $event] = [[$event], get_class($event)];
  5. }
  6. return [$event, Arr::wrap($payload)];
  7. }

该方法主要是为了解析一下事件名和参数,$event 解析为字符串,$payload 解析为数组。当参数 $event 为对象时, laravel 会解析出类名作为事件名称,并且将类实例作为 $payload 数组参数返回

  1. public function getListeners($eventName)
  2. {
  3. $listeners = $this->listeners[$eventName] ?? [];
  4. $listeners = array_merge(
  5. $listeners,
  6. $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
  7. );
  8. return class_exists($eventName, false)
  9. ? $this->addInterfaceListeners($eventName, $listeners)
  10. : $listeners;
  11. }
  12. protected function getWildcardListeners($eventName)
  13. {
  14. $wildcards = [];
  15. foreach ($this->wildcards as $key => $listeners) {
  16. if (Str::is($key, $eventName)) {
  17. $wildcards = array_merge($wildcards, $listeners);
  18. }
  19. }
  20. return $this->wildcardsCache[$eventName] = $wildcards;
  21. }
  22. protected function addInterfaceListeners($eventName, array $listeners = [])
  23. {
  24. foreach (class_implements($eventName) as $interface) {
  25. if (isset($this->listeners[$interface])) {
  26. foreach ($this->listeners[$interface] as $names) {
  27. $listeners = array_merge($listeners, (array) $names);
  28. }
  29. }
  30. }
  31. return $listeners;
  32. }
  1. getWildcardListeners 方法会解析 wildcards 中的监听事件,这一部分主要是带通配符的事件名称,并且解析完后会进行内存缓存
  2. addInterfaceListeners 方法会向上发散,会找到事件类所有实现的接口类,并且进一步解析 listeners 中是否有对应接口类的监听器函数,借此实现了类似 JavaScript 中的事件冒泡原理
  3. 获取到事件的所有监听器函数之后,会按照顺序依次调用,由参数 halt 或者 监听器函数返回值( false ) 来决定是否停止继续执行剩余监听器代码,所以,在绑定事件监听器时,绑定的顺序也是很重要的

Dispatcher 的其余代码

打开 Dispatcher 实现的接口类,发现还有一些其他方法,例如:push、flush、forget、hasListeners 等等,这些都是一些辅助的方法函数,都是对 listen / dispatch 的调用,或者是对 listeners / wildcards / wildcardsCache 的处理

laravel 对队列的使用

事件注册机制

通过上述分析,事件注册本质上就是调用 listener 函数,进行事件名和事件处理函数的关系绑定。

  1. $event = $app->make("events");
  2. // 绑定事件名称 和 类字符串
  3. $event->listen('order',App\Listeners\OrderListenerOne::class);
  4. $event->listen(App\Events\OrderEvent::class,App\Listeners\OrderListenerOne::class);
  5. // 绑定事件名称 和 闭包函数
  6. $event->listen('order',function( $a , $b ){
  7. echo "<hr>";
  8. echo $a, "<br>";
  9. echo $b, "<br>";
  10. echo "<hr>";
  11. });

事件触发调度

  1. // 字符串名称触发
  2. $event->dispatch("order",[1,11,22]);
  3. // 类事件触发
  4. $one = new App\Events\OrderEvent(1);
  5. $event->dispatch($one,[1,11,22]); // 如果是类的话,后边参数会被类覆盖,

说明

  1. laravel 事件处理,每个地方都在依据是否带有通配符进行分情况处理,带通配符的话,会将事件名作为参数传递
  2. 如果触发事件的是类实例,laravel 会解析出类名作为事件名称
  3. laravel 的事件也有向上冒泡功能


评论 0

发表评论

Top