事件

上一节,我们讲述了 Component 关于“属性” 和 ServiceLocator 的实现。

今天我们来看看事件的实现机制。

从【yii2实战系列中对事件的理解】,我们可以把事件分别三种

实例事件
类级别事件
全局事件
下面我们将依次对这三种事件的实现进行分析。

前两天看到有同学还在【yii2实战系列】的评论区留言 “事件的触发跟方法调用有什么区别”,十九大刚刚闭幕,这认识可不够深刻哈。

有些同学喜欢抱怨,事件不会用,源码更是看不懂啊看不懂。

看不懂只能说明,我这篇文章写的晚了。

其实,按照我们先前对依赖注入容器或者服务定位容器的实现分析,事件的实现逻辑你应该能猜个十有八九,无非就是先通过一个方法对某个事件注册相关的回调函数,触发事件的时候,再通过事件ID找到注册的事件进行回调,其实就是这么简单。

实例事件

下面我们以一个demo为例先回顾下事件是怎么玩的。

frontend/components 目录下创建类 Foo

1
2
3
4
5
6
7
8
9
10
11
<?php

namespace frontend\components;

use Yii;
use yii\base\Component;

class Foo extends Component
{
const EVENT_HELLO = 'hello';
}

继续在该目录下创建两个类 FooT 和 BarT,用于触发事件时的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace frontend\components;

use Yii;

class FooT
{
public function hello()
{
var_dump(__METHOD__);
}
}

class BarT
{
public function hello()
{
var_dump(__METHOD__);
}
}

按照习惯,FooT 和 BarT 最好分别写在 FooT.php 和 BarT.php 文件内,上面为了省空间,写在了一起。为了看起来简单一些,我们只是让 Foo 类继承了 yii\base\Component,这是必要的,FooT 和 BarT 是单纯的类。

我们在 site/index 操作内注册事件并触发事件

1
2
3
4
5
6
7
8
9
10
use frontend\components\Foo;

$foo = new Foo;

// 注册事件
$foo->on(Foo::EVENT_HELLO, ['frontend\components\FooT', 'hello']);
$foo->on(Foo::EVENT_HELLO, ['frontend\components\BarT', 'hello']);

// 触发事件
$foo->trigger(Foo::EVENT_HELLO);

注意看,我们给 Foo::EVENT_HELLO注册了两个事件,毫无疑问,你将会在页面上看到如下打印的信息,这是我们分别在 FooT::hello 和 BarT::hello 内输出的信息。

1
2
string(31) "frontend\components\FooT::hello"
string(31) "frontend\components\BarT::hello"

上面这个demo你应该不会感觉到陌生,如果感觉还有些问题,可以回去再把实战系列的事件再看上一看。

下面我们来分析下上述实例的实现。

首先你应该注意到了,frontend\components\Foo 继承自 yii\base\Component,前面我们反复介绍过 yii2 内部对属性 事件 行为的实现离不开 yii\base\Component ,这不这里又用到了。

yii\base\Component::on 方法,为我们提供注册事件的功能,注册事件的实质,是把回调函数,我们称之为事件处理程序,绑定到该事件上。

yii\base\Component::on 方法的实现

1
2
3
4
5
6
7
8
9
public function on($name, $handler, $data = null, $append = true)
{
$this->ensureBehaviors();
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
array_unshift($this->_events[$name], [$handler, $data]);
}
}

我们看到,yii\base\Component::on 方法有4个参数

$name, 事件的名字,这里是用 Foo::EVENT_HELLO 表示的字符串 “hello”
$handler, 事件处理程序,回调函数的意思,为后面触发事件时,可通过call_user_func方法调用
$data, 可以在我们触发事件时,为事件处理程序内传递的数据
$append, 是说我们新注册的事件位于事件处理程序列表的首还是尾,比如这里我们同时给事件 Foo::EVENT_HELLO绑定了两个事件处理程序,触发的时候,默认按照先注册先触发的逻辑,如果我们想让后面绑定的先触发,就可以设置这个参数是false
yii\base\Component::ensureBehaviors ,这里是给 frontend\components\Foo 实例绑定行为,后面我们介绍行为的时候再说。

on方法的实质,是在程序运行时,把注册的实例相关的事件处理程序储存在 yii\base\Component::_events[$name] 数组内,注意这里是个数组哦,所以我们可以为 Foo::EVENT_HELLO 添加多个事件处理程序。

on方法处理之后,yii\base\Component::_events 的数据结构,我们贴一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Array
(
[hello] => Array
(
[0] => Array
(
[0] => Array
(
[0] => frontend\components\FooT
[1] => hello
)

[1] =>
)

[1] => Array
(
[0] => Array
(
[0] => frontend\components\BarT
[1] => hello
)

[1] =>
)

)

)

刚才我们提到过,on方法的第三个参数,是可以通过调用回调函数时传递的数据,即 yii\base\Component::_event[‘hello’][0][1] 单元所指,事件处理程序即 yii\base\Component::_event[‘hello’][0][0] 单元所指,对吧?

接着看事件的触发。

事件的触发,其实就是调用我们给事件ID注册的事件处理程序。

yii\base\Component::trigger 方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();
if (!empty($this->_events[$name])) {
if ($event === null) {
$event = new Event;
}
if ($event->sender === null) {
$event->sender = $this;
}
$event->handled = false;
$event->name = $name;
foreach ($this->_events[$name] as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
if ($event->handled) {
return;
}
}
}
// invoke class-level attached handlers
Event::trigger($this, $name, $event);
}

trigger 方法有两个参数,第一个参数是事件ID,第二个参数我们稍后介绍。

从 if 看起,这里会先判断实例的事件处理程序列表中有没有叫 $name 的事件,如果没有,会继续触发类级别的事件处理程序,这个等一下说,到目前为止,我们介绍的是实例的事件机制,所谓实例事件,即都是针对某个实例进行的事件操作。比如我们这里demo是针对类 Foo 的实例进行的操作。

回过来看trigger函数的第二个参数,很明显,第二个参数要求是null或者是 yii\base\Event的实例,从 if ($event === null) { $event = new Event; } 可以看出,第二个参数为null的实质,$event 其实也是 yii\base\Event 的实例。

$event有什么用呢?或者说这样设计的目的是什么?

这我们要看 $event 用在哪里了。

yii\base\Component::trigger方法中,$event 用到了四处

$event->data = $handler[1],存储数据
call_user_func($handler[0], $event),把 $event 传递给事件回调函数
if ($event->handled) { return; },如果trigger时设置的 event实例的handled属性为真,则该事件后续未调用的其他回调将不会被触发
Event::trigger($this, $name, $event),传递给类级别的事件触发
在yii2中有很多诸如此类的实现,比如 yii\web\User 类中 beforeLogin 和 afterLogin 中有如下类似代码

1
2
3
4
5
$this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([
'identity' => $identity,
'cookieBased' => $cookieBased,
'duration' => $duration,
]));

这样就可以在注册的回调函数内获取到相关的数据了,很方便。

当然,事件还可以移除,其效果就是从 yii\base\Component::_events 属性中移除相应事件,代码参考 yii\base\Component::off 方法。

类级别事件的实现

我们知道,类级别事件设计的初衷是为了让类以及其子类的所有的实例都能够触发事件,感觉很神奇的样子,其实跟实例事件的实现大同小异,来简单的看一下。

类级别事件的实现,主要依靠下面三个方法

yii\base\Event::on 注册事件
yii\base\Event::trigger 触发事件
yii\base\Event::off 移除事件
yii\base\Event::on ,注册事件,其实就是通过on方法,在 yii\base\Event::_events[$name][$class][] 数组中添加事件处理程序

yii\base\Event::trigger,触发事件,自然就是调用 上一步注册的事件的处理程序,但是怎么保证类和子类的的所有实例都触发呢?

这取决于下面这行代码的实现

1
2
3
4
5
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);

当实例调用 yii\base\Component::trigger 方法触发事件的时候,最后会调用 yii\base\Event::trigger($this, $name, $event),触发类级别的事件,整个过程应该不难理解。

但是有一点需要注意,像yii2复杂的父类层级或者更深的层级,如果我们给越底层的父类注册事件,是不是就会影响越多的子类?毋庸置疑,我们应该尽量避免底层类注册类级别事件。

全局事件

事实上,全局事件指的就是实例事件,不同的是,绑定事件的实例,全局事件的对象是一个全局可访问的实例,你肯定猜到了,是 yii\web\Application的实例 Yii::$app。

所以,有时候,我们通过 Yii::$app->on 注册的事件,在程序代码的任意位置都可以通过 Yii::$app->trigger 触发,这里的on和 trigger方法指的是 yii\base\Component::on 和 yii\base\Component::trigger。

事件我们就介绍到这里,下一节我们来谈一谈“行为”的实现。