资讯专栏INFORMATION COLUMN

扒一扒 EventServiceProvider 源代码

maochunguang / 2519人阅读

摘要:有了之前的简述的使用,大致了解了的使用。今天我们就来扒一扒的源码。好了,整个和就关联在一起了。注下篇文章我们再来扒一扒源码当把事件广播出去后,我们就开始执行该事件的各个监听了。

有了之前的《简述 Laravel Model Events 的使用》https://mp.weixin.qq.com/s/XrhDq1S5RC9UdeULVVksoA,大致了解了 Event 的使用。

今天我们就来扒一扒 Event 的源码。

开始之前,需要说下两个 EventServiceProvider 的区别:

AppProvidersEventServiceProvider

IlluminateEventsEventServiceProvider

第一个 AppProvidersEventServiceProvider 主要是定义 eventlistener 的关联;第二个 IlluminateEventsEventServiceProviderLaravel 的三大基础 ServiceProvider 之一,主要负责「分派」工作。

好了,下面开始具体的分析工作。

AppProvidersEventServiceProvider

主要是定义 eventlistener 的关联,如:

 [
            "AppListenersRssPublicListener1",
        ],
        "AppEvents*Event" => [
            "AppListenersRssPublicListener2",
            "AppListenersRssPublicListener3",
        ],
        "IlluminateContractsBroadcastingShouldBroadcast" => [
            "AppListenersRssPublicListener4",
            "AppListenersRssPublicListener5",
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();
    }
}

主要继承 IlluminateFoundationSupportProvidersEventServiceProvider

listens() as $event => $listeners) {
            foreach ($listeners as $listener) {
                Event::listen($event, $listener);
            }
        }

        foreach ($this->subscribe as $subscriber) {
            Event::subscribe($subscriber);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function register()
    {
        //
    }

    /**
     * Get the events and handlers.
     *
     * @return array
     */
    public function listens()
    {
        return $this->listen;
    }
}

把定义 eventlistener 的关联交给用户自己去做,然后父类 EventServiceProvider 只是做关联工作,在 boot() 中:

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);
    }
}

这里主要看两个函数:

Event::listen($event, $listener);

Event::subscribe($subscriber);

就这么简单,我们说完了第一个 EventServiceProvider ,我们开始第二个。

IlluminateEventsEventServiceProvider

看过之前文章的知道,Event 有个全局函数:

Artisan::command("public_echo", function () {
    event(new RssPublicEvent());
})->describe("echo demo");

...

if (! function_exists("event")) {
    /**
     * Dispatch an event and call the listeners.
     *
     * @param  string|object  $event
     * @param  mixed  $payload
     * @param  bool  $halt
     * @return array|null
     */
    function event(...$args)
    {
        return app("events")->dispatch(...$args);
    }
}

IlluminateEventsEventServiceProvider,是 Laravel 三个基础 ServiceProvider 之一:

/**
 * Register all of the base service providers.
 *
 * @return void
 */
protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}

我们接着看 IlluminateEventsEventServiceProvider

app->singleton("events", function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

它注册了单例形式,并创建和返回 Dispatcher 对象:

use IlluminateContractsEventsDispatcher as DispatcherContract;
use IlluminateContractsBroadcastingFactory as BroadcastFactory;
use IlluminateContractsContainerContainer as ContainerContract;
class Dispatcher implements DispatcherContract
{
    /**
     * The IoC container instance.
     *
     * @var IlluminateContractsContainerContainer
     */
    protected $container;

    /**
     * The registered event listeners.
     *
     * @var array
     */
    protected $listeners = [];

    /**
     * The wildcard listeners.
     *
     * @var array
     */
    protected $wildcards = [];

    /**
     * The queue resolver instance.
     *
     * @var callable
     */
    protected $queueResolver;
    
...
}    

主要实现 Dispatcher 接口:


下面我们来解说每一个函数。

listen()
Register an event listener with the dispatcher.
public function listen($events, $listener)
{
    foreach ((array) $events as $event) {
        if (Str::contains($event, "*")) {
            $this->wildcards[$event][] = $this->makeListener($listener, true);
        } else {
            $this->listeners[$event][] = $this->makeListener($listener);
        }
    }
}

这就好理解了,把通配符的放在 wildcards 数组中,另一个放在 listeners 数组中。接下来看函数 makeListener()

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));
    };
}

如果传入的 $listener 为字符串,则执行函数 createClassListener

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
        );
    };
}

先来看看函数 createClassCallable()

protected function createClassCallable($listener)
{
    list($class, $method) = Str::parseCallback($listener, "handle");

    if ($this->handlerShouldBeQueued($class)) {
        return $this->createQueuedHandlerCallable($class, $method);
    }

    return [$this->container->make($class), $method];
}

第一个函数还是很好理解:

public static function parseCallback($callback, $default = null)
{
    return static::contains($callback, "@") ? explode("@", $callback, 2) : [$callback, $default];
}

就看传入的 listener 是不是 class@method 结构,如果是就用 @ 分割,否则就默认的就是 class 类名,然后 method 就是默认的 handle 函数 —— 这也是我们创建 Listener 类提供的做法。

接着就看是否可以放入队列中:

protected function handlerShouldBeQueued($class)
{
    try {
        return (new ReflectionClass($class))->implementsInterface(
            ShouldQueue::class
        );
    } catch (Exception $e) {
        return false;
    }
}

也就判断该 listener 类是否实现了接口类 ShouldQueue。如果实现了,则可以将该类放入队列中 (返回闭包函数):

protected function createQueuedHandlerCallable($class, $method)
{
    return function () use ($class, $method) {
        $arguments = array_map(function ($a) {
            return is_object($a) ? clone $a : $a;
        }, func_get_args());

        if ($this->handlerWantsToBeQueued($class, $arguments)) {
            $this->queueHandler($class, $method, $arguments);
        }
    };
}

我们接着看 handlerWantsToBeQueued

protected function handlerWantsToBeQueued($class, $arguments)
{
    if (method_exists($class, "shouldQueue")) {
        return $this->container->make($class)->shouldQueue($arguments[0]);
    }

    return true;
}

所以说,如果在 listener 类中写了 shouldQueue 方法,则就看该方法是不是返回 true 或者 false 来决定是否放入队列中:

protected function queueHandler($class, $method, $arguments)
{
    list($listener, $job) = $this->createListenerAndJob($class, $method, $arguments);

    $connection = $this->resolveQueue()->connection(
        $listener->connection ?? null
    );

    $queue = $listener->queue ?? null;

    isset($listener->delay)
                ? $connection->laterOn($queue, $listener->delay, $job)
                : $connection->pushOn($queue, $job);
}
注:和队列相关的放在之后再做分析,此处省略

好了,回到开始的地方:

// createClassCallable($listener)
return [$this->container->make($class), $method];

到此,也就明白了,如果是 通配符 的,则对应的执行函数 (默认的为: handle) 传入的参数有两个:$event 事件对象和 $payload;否则对应执行函数,传入的参数就只有一个了:$payload

同理,如果传入的 listener 是个函数的话,返回的闭包就这样的:

return function ($event, $payload) use ($listener, $wildcard) {
    if ($wildcard) {
        return $listener($event, $payload);
    }

    return $listener(...array_values($payload));
};

整个流程就通了,listener 函数的作用就是:在 Dispatcher 中的 $listeners$wildcards 的数组中,存储 ["event" => Callback] 的结构数组,以供执行使用。

说完了第一个函数 Event::listen(),第二个函数了:Event::subscribe(),留着之后再说。

好了,整个 eventlistener 就关联在一起了。接下来就开始看执行方法了。

dispatch()
Dispatch an event and call the listeners.

正如上文的 helpers 定义的,所有 Event 都是通过该函数进行「分发」事件和调用所关联的 listeners

/**
 * Fire an event and call the listeners.
 *
 * @param  string|object  $event
 * @param  mixed  $payload
 * @param  bool  $halt
 * @return array|null
 */
public function dispatch($event, $payload = [], $halt = false)
{
    // When the given "event" is actually an object we will assume it is an event
    // object and use the class as the event name and this event itself as the
    // payload to the handler, which makes object based events quite simple.
    list($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 a response is returned from the listener and event halting is enabled
        // we will just return this response, and not call the rest of the event
        // listeners. Otherwise we will add the response on the response list.
        if ($halt && ! is_null($response)) {
            return $response;
        }

        // If a boolean false is returned from a listener, we will stop propagating
        // the event to any further listeners down in the chain, else we keep on
        // looping through the listeners and firing every one in our sequence.
        if ($response === false) {
            break;
        }

        $responses[] = $response;
    }

    return $halt ? null : $responses;
}

先理解注释的函数 parseEventAndPayload()

When the given "event" is actually an object we will assume it is an event object and use the class as the event name and this event itself as the payload to the handler, which makes object based events quite simple.
protected function parseEventAndPayload($event, $payload)
{
    if (is_object($event)) {
        list($payload, $event) = [[$event], get_class($event)];
    }

    return [$event, Arr::wrap($payload)];
}

如果 $event 是个对象,则将 $event 的类名作为事件的名称,并将该事件 [$event] 作为 $payload

接着判断 $payload 是否可以「广播」出去,如果可以,那就直接广播出去:

protected function shouldBroadcast(array $payload)
{
    return isset($payload[0]) &&
           $payload[0] instanceof ShouldBroadcast &&
           $this->broadcastWhen($payload[0]);
}

就拿上文的例子来说吧:

 "public_channel_".Carbon::now()->toDateTimeString()];
    }
}

首先它实现接口 ShouldBroadcast,然后看是不是还有额外的条件来决定是否可以广播:

/**
 * Check if event should be broadcasted by condition.
 *
 * @param  mixed  $event
 * @return bool
 */
protected function broadcastWhen($event)
{
    return method_exists($event, "broadcastWhen")
            ? $event->broadcastWhen() : true;
}

由于本实例没有实现 broadcastWhen 方法,所以返回默认值 true

所以可以将本实例广播出去:

/**
 * Broadcast the given event class.
 *
 * @param  IlluminateContractsBroadcastingShouldBroadcast  $event
 * @return void
 */
protected function broadcastEvent($event)
{
    $this->container->make(BroadcastFactory::class)->queue($event);
}

这就交给 BroadcastManager 来处理了,此文不再继续深挖。

:下篇文章我们再来扒一扒 BroadcastManager 源码

当把事件广播出去后,我们就开始执行该事件的各个监听了。通过之前的文章知道,一个 Event,不止一个 Listener 监听,所以需要通过一个 foreach 循环来遍历执行 Listener,首先获取这些 Listener

/**
 * Get all of the listeners for a given event name.
 *
 * @param  string  $eventName
 * @return array
 */
public function getListeners($eventName)
{
    $listeners = $this->listeners[$eventName] ?? [];

    $listeners = array_merge(
        $listeners, $this->getWildcardListeners($eventName)
    );

    return class_exists($eventName, false)
                ? $this->addInterfaceListeners($eventName, $listeners)
                : $listeners;
}

该方法主要通过三种方式累加获取所有 listeners:该类中的属性:$listeners$wildcards,以及如果该 $event 是个对象的,还包括该类的所有接口关联的 listeners 数组。

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;
}
注:class_implements — 返回指定的类实现的所有接口。

接下来就是执行每个 listener 了:

$response = $listener($event, $payload);

// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && ! is_null($response)) {
    return $response;
}

// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
    break;
}

$responses[] = $response;

由上文可以知道 $listener,实际上就是一个闭包函数,最终的结果相当于执行 handle 函数:

public function handle(RssPublicEvent $event)
{
    info("listener 1");
}

...

public function handle(RssPublicEvent $event, array $payload)
{
    info("listener 2");
}
写个 demo

我们写个 demo,在 EventServiceProviderlisten 数组,填入这三种方式的关联情况:

protected $listen = [
    "AppEventsRssPublicEvent" => [
        "AppListenersRssPublicListener1",
    ],
    "AppEvents*Event" => [
        "AppListenersRssPublicListener2",
        "AppListenersRssPublicListener3",
    ],
    "IlluminateContractsBroadcastingShouldBroadcast" => [
        "AppListenersRssPublicListener4",
        "AppListenersRssPublicListener5",
    ],
];

然后在每个 RssPublicListener*handle 方法输出对应的值,最后运行 php artisan public_echo,看结果:

[2018-10-06 20:05:57] local.INFO: listener 1  
[2018-10-06 20:05:58] local.INFO: listener 2  
[2018-10-06 20:05:59] local.INFO: listener 3  
[2018-10-06 20:05:59] local.INFO: listener 4  
[2018-10-06 20:06:00] local.INFO: listener 5
其他函数

说完了执行函数,基本上也就说完了整个 Event 事件流程了。剩下的只有一些附属函数,一看基本都理解:

/**
 * Register an event and payload to be fired later.
 *
 * @param  string  $event
 * @param  array  $payload
 * @return void
 */
public function push($event, $payload = [])
{
    $this->listen($event."_pushed", function () use ($event, $payload) {
        $this->dispatch($event, $payload);
    });
}

/**
 * Flush a set of pushed events.
 *
 * @param  string  $event
 * @return void
 */
public function flush($event)
{
    $this->dispatch($event."_pushed");
}

/**
 * Determine if a given event has listeners.
 *
 * @param  string  $eventName
 * @return bool
 */
public function hasListeners($eventName)
{
    return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
}

/**
 * Fire an event until the first non-null response is returned.
 *
 * @param  string|object  $event
 * @param  mixed  $payload
 * @return array|null
 */
public function until($event, $payload = [])
{
    return $this->dispatch($event, $payload, true);
}

/**
     * Remove a set of listeners from the dispatcher.
 *
 * @param  string  $event
 * @return void
 */
public function forget($event)
{
    if (Str::contains($event, "*")) {
        unset($this->wildcards[$event]);
    } else {
        unset($this->listeners[$event]);
    }
}

/**
 * Forget all of the pushed listeners.
 *
 * @return void
 */
public function forgetPushed()
{
    foreach ($this->listeners as $key => $value) {
        if (Str::endsWith($key, "_pushed")) {
            $this->forget($key);
        }
    }
}
总结

Event 做了比较详细的梳理,大致了解了它的整个流程,下一步就是看看怎么和队列结合在一起,和利用「观察者模式」的那部分代码逻辑了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/29491.html

相关文章

  • BEM实战之一扒淘票票页面

    摘要:扒一扒淘票票界面淘票票界面写的挺美观的,但是最近看了看淘票票的命名方式,觉得稍有不妥。命名与页面内容挂钩,代码复用性低。 BEM解析 BEM是一套CSS国际命名规范,是一个非常有用的功能强大且简单的命名约定,它能使前端代码更易读,易于理解易于扩展。BEM是块(block)、元素(element)、修饰符(modifier)的缩写。 B:Block是块,一个独立的组件,将所有东西都划分...

    godlong_X 评论0 收藏0
  • 一扒随机数(Random Number)的诞生历史

    摘要:年成立的为互联网提供真正的随机数。在年,随机数市场发生了一个巨大的变化,在其芯片组上集成了芯片级的随机数生成器。 作者:Alon Zakai 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58cfc3dda6d8a07e449fdd29 英文原文:A Brief History of Random Number...

    AlienZHOU 评论0 收藏0
  • 一扒Rancher社区中的小工具

    摘要:可是并没有统一的版本号管理功能,只是额外提供了内包的依赖路径。描述文件支持两种格式,普通方式和方式,可以直接在其中描述依赖库的远程地址版本号等,一个简单的例子我这里使用普通格式然后在根目录执行,即可获得相关版本的依赖包非常轻量级,非常简洁。 与Linux、OpenStack等成熟的技术社区相比,Rancher社区还是处于初级发展阶段,一个技术社区的成败并不是单纯的代码贡献,而学习文档的...

    wwolf 评论0 收藏0
  • 如何优雅地使用 VSCode 来编辑 vue 文件?

    摘要:注本文首发在我的个人博客最近有个项目使用了,本来一直使用的是来进行开发,可是遇到了很多问题。此外,还有很多规范是帮助我们写出更加优雅而不容易出错的代码的。,终于基本搞定了,可以愉快地开发应用了。 注:本文首发在 我的个人博客 最近有个项目使用了 Vue.js ,本来一直使用的是 PHPStorm 来进行开发,可是遇到了很多问题。 后来,果断放弃收费的 PHPStorm ,改用 vsco...

    pekonchan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<