前面分析完一个请求的简单生命周期,其中涵盖了依赖注入、中间件的分析,这些不在单独分析。接下来将分析框架的事件机制。
项目根目录命令行运行:php think make:listen ShowAppInit
创建一个监听器,这将在app\listener
目录下生成一个ShowAppInit.php
文件(如果没有listener
目录则创建之)。接着简单修改ShowAppInit.php
文件的代码如下:
<?php
namespace app\listener;
class ShowAppInit
{
public function handle($event)
{
echo "App 初始化啦" .PHP_EOL;
}
}
监听器创建完成后,将其添加到app
目录下的event.php
文件:
return [
'bind' => [
],
'listen' => [
'AppInit' => [ 'app\listener\ShowAppInit' ], //添加在这里
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];
这样就绑定了一个监听器(观察者)到AppInit
事件,一旦该事件被触发,监听器将开始工作——执行其handle
方法下的代码。
上面绑定监听器后,系统是在哪里载入了这些配置呢?
顺着一个请求的生命周期:Http::run()→Http::runWithRequest()→Http::initialize()->App::initialize()→App::load(),发现在load
方法有这样几行:
if (is_file($appPath . 'event.php')) {
$this->loadEvent(include $appPath . 'event.php');
}
就是在这个位置,执行loadEvent
方法加载事件的配置——该方法代码如下:
public function loadEvent(array $event): void
{
if (isset($event['bind'])) {
// 将事件标识到事件(操作,比如一个控制器操作)的映射合并到「Event」类「$bing」成员变量中
// 比如 'UserLogin' => 'app\event\UserLogin',
$this->event->bind($event['bind']);
}
if (isset($event['listen'])) {
// 合并所有观察者(监听者)到Event类的listener数组
// 其形式为实际事件(被观察者)到观察者的映射
$this->event->listenEvents($event['listen']);
}
if (isset($event['subscribe'])) {
// 订阅,实际上是一个批量的监听
// 就像一个人他同时订阅天气预报、股市行情、小花上QQ了……
// 一个订阅器,里面可以实现多个事件的监听
// 比如,我在一个订阅器中,同时监听用户登录,用户退出等操作
$this->event->subscribe($event['subscribe']);
}
}
最终得到的Event
类对象大概如下:
事件监听器绑定到事件之后,框架在初始化过程中,将这些配置加载到Event
类的对象(当然也可以在程序中手动绑定监听器),接下来就可以决定在何时触发事件。AppInit
事件是在App::initialize()
方法中触发的,其代码如下:
$this->event->trigger('AppInit');
接着,我们看看trigger
方法是如何触发事件的(如何调用监听器的handle
方法)——其代码如下:
public function trigger($event, $params = null, bool $once = false)
{
// A 如果设置了关闭事件,则直接返回,不再执行任何监听器
if (!$this->withEvent) {
return;
}
// B
// 如果是一个对象,解析出对象的类
if (is_object($event)) {
//将对象实例作为传入参数
$params = $event;
$event = get_class($event);
}
//根据事件标识解析出实际的事件
if (isset($this->bind[$event])) {
$event = $this->bind[$event];
}
$result = [];
// 解析出事件的监听者(可多个)
$listeners = $this->listener[$event] ?? [];
foreach ($listeners as $key => $listener) {
// C
// 执行监听器的操作
$result[$key] = $this->dispatch($listener, $params);
// 如果返回false,或者没有返回值且 $once 为 true,直接中断,不再执行后面的监听器
if (false === $result[$key] || (!is_null($result[$key]) && $once)) {
break;
}
}
// 是否返回多个监听器的结果
// $once 为 false 则返回最后一个监听器的结果
return $once ? end($result) : $result;
}
trigger
方法首先通过$this->withEvent
判断监听器是否要执行,如果为否,则直接终止该方法。 withEvent
的值可以通过如下方法设定:
app.with_event
的值。该值在Http::runWithRequest()
方法中读取进来:$this->app->event->withEvent($this->app->config->get('app.with_event', true));
由此,我们可以在配置文件中全局开启或者关闭事件机制。
Event::withEvent
方法设置。由此也可以得知,我们在执行完一个监听器之后,可以通过Event::withEvent
方法设置后面的监听器是否还要执行。这里传给trigger
方法的是一个字符串(事件标识)AppInit
,通过$event = $this->bind[$event]
得到$event
的值为think\event\AppInit
,再将该值作为键,$listeners = $this->listener[$event]
,从listener
数组中获取实际的监听器,这里将得到$listeners
为[app\listener\ShowAppInit]
。
这里主要看dispatch
方法:
protected function dispatch($event, $params = null)
{
// 如果不是字符串,比如,一个闭包
if (!is_string($event)) {
$call = $event;
//一个类的静态方法
} elseif (strpos($event, '::')) {
$call = $event;
} else {
$obj = $this->app->make($event);
$call = [$obj, 'handle'];
}
return $this->app->invoke($call, [$params]);
}
不管是闭包、静态类方法,还是监听器的handle
方法,都是通过invoke
方法来执行,invoke
方法实现如下:
public function invoke($callable, array $vars = [], bool $accessible = false)
{
// 如果$callable是闭包
if ($callable instanceof Closure) {
return $this->invokeFunction($callable, $vars);
}
// $callable不是闭包的情况
return $this->invokeMethod($callable, $vars, $accessible);
}
最终通过PHP反射类来执行对应的方法。
从代码实现可以看出,如果一个监听器方法最终返回false,或者没有返回值且 $once 为 true,则不再执行后面的监听器。trigger
方法是返回多个监4听器的执行结果还是最后一个,由最后一个参数$once
决定,$once
为true
,只返回最后一个监听器执行结果,反之,返回所有结果组成的数组。
注意到监听器的handle
方法还可以接收一个参数,从上面的分析可知,trigger
方法的第二个参数最终将传给handle
方法。
那么,在什么情况下需要用到这个参数呢?举个例子,假如要监听一个用户登录,我们可以新建一个监听器,绑定事件标识,在handle
方法中实现业务逻辑——例如,输出:「有用户登录啦」,然后在登录代码的后面trigger
这个事件标识。但如果我们又要知道是谁登录的话,这时我们可以把用户名作为trigger
的第二个参数传入,在监听器的handle
方法可以这样使用:echo $event . 用户登录啦
。
当然,这里的$event
也可以是一个事件类对象。
这里的订阅,本质上一种「复合」的监听器,比如,小明同时要监听小花跑、跳、吃饭、睡觉,这时,就可以把小明要针对这些动作做出的反应都放在一个类里面,方便管理。
在app
目录下的event.php
文件中的listen
键添加两个事件标识,如下所示:
'listen' => [
'UserLogin' => [],
'UserLogout' => [],
],
如果没有这两个事件标识,订阅类的方法将无法被添加到Event
类的$listener
数组,导致最后无法执行到订阅类的方法的。
创建一个订阅类
项目根目录下,命令行运行:php think make:subscribe User
,会在app/subscribe
目录下创建一个订阅类,在该类中添加以下方法(如代码所示):
class User
{
public function onUserLogin(){
echo '我知道用户登录了,因为我订阅了<br>';
}
public function onUserLogout(){
echo '我知道用户退出了,因为我订阅了<br>';
}
}
创建控制器
在app/controller
目录下创建一个User
控制器,添加代码如下:
class User
{
public function __construct(){
//添加一个订阅类
Event::subscribe(\app\subscribe\User::class);
}
public function login(){
echo "用户登录了<br> ";
Event::trigger('UserLogin');
}
public function logout(){
echo "用户退出了<br> ";
Event::trigger('UserLogout');
}
}
假如访问User
控制器的login
操作,调试运行代码到Event::subscrible()
方法,该方法代码如下:
public function subscribe($subscriber)
{
if (!$this->withEvent) {
return $this;
}
// 强制转换为数组
$subscribers = (array) $subscriber;
foreach ($subscribers as $subscriber) {
if (is_string($subscriber)) {
//实例化事件订阅类
$subscriber = $this->app->make($subscriber);
}
// 如果该事件订阅类存在'subscribe'方法,执行该方法
if (method_exists($subscriber, 'subscribe')) {
// 手动订阅
$subscriber->subscribe($this);
} else {
// 智能订阅
$this->observe($subscriber);
}
}
return $this;
}
这里关键看observe
方法:
public function observe($observer, string $prefix = '')
{
if (!$this->withEvent) {
return $this;
}
if (is_string($observer)) {
$observer = $this->app->make($observer);
}
$reflect = new ReflectionClass($observer);
$methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC);
// 支持订阅类中的方法指定前缀
if (empty($prefix) && $reflect->hasProperty('eventPrefix')) {
$reflectProperty = $reflect->getProperty('eventPrefix');
$reflectProperty->setAccessible(true);
$prefix = $reflectProperty->getValue($observer);
}
foreach ($methods as $method) {
$name = $method->getName();
if (0 === strpos($name, 'on')) {
// 自动将订阅类的方法添加到监听器
$this->listen($prefix . substr($name, 2), [$observer, $name]);
}
}
return $this;
}
运行过程见上面的注释。最后的结果大概是这样的:
在UserLogin
和UserLogout
事件标识下,分别添加了对应的监听器,分别是事件订阅类app\subscribe\User
的onUserLogin
和onUserLogout
。
监听器添加完成后,接着是等待事件触发。login
操作中,执行了Event::trigger('UserLogin');
,这将执行app\subscribe\User
的onUserLogin
,其执行过程跟前面分析的执行事件监听器是一样的,结果输出如下:
用户登录了
我知道用户登录了,因为我订阅了
同理,访问User
控制器的logout
操作,得到:
用户退出了
我知道用户退出了,因为我订阅了
事件订阅分析完毕。总结一下,事件订阅就是一种「复合」的监听器,可以同时监听多个事件。从其实现过程来看,本质和事件监听器是一样的,个人认为,使用事件订阅的好处是仅仅集中管理代码,把对某个对象(被观察者)的多个动作的监听,都写在一个事件订阅类里面,因而就不用另外写相应多个动作的监听器类。
事件的实现机制,实际上是使用观察者模式实现的。观察者模式的好处是实现类的松耦合,被观察者不需要知道观察者到底做了什么,只需要触发事件就够了;另外,观察者的数量可以灵活地增加、减少,而不用修改被观察者。深入理解观察者模式,可以参考这篇:学好事件,先学学观察者模式
Bug天天改,头发日日疏,码字不易,如果有帮助到你,就点击"下方感谢"支持一下把.