说明

更新日志:2019-11-1 更新到6.0正式版

前面1-9篇分析完一个请求的简单生命周期,其中涵盖了依赖注入、中间件的分析,这些不在单独分析。接下来将分析框架的事件机制。

事件配置文件的载入

准备工作

项目根目录命令行运行:php think make:listen ShowAppInit创建一个监听器,这将在app\listener目录下生成一个ShowAppInit.php文件(如果没有listener目录则创建之)。接着简单修改ShowAppInit.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;

}

A 决定是否继续执行监听器

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方法设置后面的监听器是否还要执行。

B 事件标识解析

这里传给trigger方法的是一个字符串(事件标识)AppInit,通过$event = $this->bind[$event] 得到$event的值为think\event\AppInit,再将该值作为键,$listeners = $this->listener[$event],从listener 数组中获取实际的监听器,这里将得到$listeners为[app\listener\ShowAppInit]。

C 执行监听器

这里主要看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反射类来执行对应的方法。

D 是否中断和返回结果

从代码实现可以看出,如果一个监听器方法最终返回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 '我知道用户登录了,因为我订阅了
';

}

public function onUserLogout(){

echo '我知道用户退出了,因为我订阅了
';

}

}

创建控制器

在app/controller目录下创建一个User控制器,添加代码如下:

class User

{

public function __construct(){

//添加一个订阅类

Event::subscribe(\app\subscribe\User::class);

}

public function login(){

echo "用户登录了
";

Event::trigger('UserLogin');

}

public function logout(){

echo "用户退出了
";

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操作,得到:

用户退出了

我知道用户退出了,因为我订阅了

事件订阅分析完毕。总结一下,事件订阅就是一种「复合」的监听器,可以同时监听多个事件。从其实现过程来看,本质和事件监听器是一样的,个人认为,使用事件订阅的好处是仅仅集中管理代码,把对某个对象(被观察者)的多个动作的监听,都写在一个事件订阅类里面,因而就不用另外写相应多个动作的监听器类。

关于观察者模式

事件的实现机制,实际上是使用观察者模式实现的。观察者模式的好处是实现类的松耦合,被观察者不需要知道观察者到底做了什么,只需要触发事件就够了;另外,观察者的数量可以灵活地增加、减少,而不用修改被观察者。深入理解观察者模式,可以参考这篇:学好事件,先学学观察者模式

本作品采用《CC 协议》,转载必须注明作者和本文链接

Was mich nicht umbringt, macht mich stärker

php ckey=6,ThinkPHP6 核心分析(十):事件相关推荐

  1. linux input子系统分析--子系统核心.事件处理层.事件传递过程

    linux input子系统分析--子系统核心.事件处理层.事件传递过程 一.  输入子系统核心分析. 1.输入子系统核心对应与/drivers/input/input.c文件,这个也是作为一个模块注 ...

  2. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  3. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 六 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  4. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  5. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  6. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  7. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 三 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  8. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  9. 数据分析与挖掘实战-家用电器用户行为分析与事件识别

    家用电器用户行为分析与事件识别 背景 居民使用家电过程中,会因为地区气候.区域不同.年龄差异,形成不同的使用习惯,若能深入了解这些习惯,针对性地开发新功能,便能开拓市场. 本案例以热水器为例,分析用户 ...

最新文章

  1. c语言主程序子程序流程图,c语言超经典矩阵式键盘的接法,流程图和扫描程序
  2. TCP/IP 建立连接的过程
  3. 【Python】编程笔记3
  4. 快速排序最好,最坏,平均复杂度分析
  5. EF+postgresql中的一些问题
  6. 利用openssl来计算sha256哈希值
  7. OpenSSF 获1000万美元投资,提升开源软件和软件供应链安全
  8. pytorh——Fully-connected
  9. 序列标注模型结果评估模块seqeval学习使用
  10. “领跑者”计划倒逼光伏产业升级效果显现
  11. 嵌入式学习(一)嵌入式c语言
  12. C-V2X 技术介绍
  13. 细数互联网企业组织架构大调整
  14. 密码学基础算法(二)中国剩余定理
  15. 时空旅行[线段树分治][维护凸壳]
  16. sklearn中的特征工程(过滤法、嵌入法和包装法)
  17. 达尔文流媒体服务器搭配
  18. 比较好用的邮箱有哪些?收费邮箱主要哪家好呢?
  19. 前端模块化(CMJ和ESM)
  20. [转]为什么我们做分布式要用redis?

热门文章

  1. jQuery源码学习(1)——addClass
  2. 2018-2019-2 网络对抗技术 20165320 Exp4 恶意代码分析
  3. 758.6G每秒:阿里云成功防御国内最大规模Memcached DDoS反射攻击
  4. 分布式存储Ceph 快速安装手册
  5. 基于Jedis实现Redis分片的理解
  6. Android实例剖析笔记(四)
  7. FACTORY设计模式【让吃货也能理解的程序】
  8. NIX***检测方法
  9. VB.Net编程实现Web Service的基础
  10. html获取cookie_知了汇智《XSS攻击-盗取cookie实战》课程文档讲解