【Yii2事件研究】事件的解绑和级别概念 [ 技术分享 ]
感谢你坚持读到这里,本篇是“Yii2事件研究”的最后一篇分享,我将继续在讲故事的同时轻松帮你学会事件机制的相关知识。
在这里我们先总结一下
忆往昔
第一篇:什么是事件,其实事件并不难,我们每天用的js都是基于事件的,点击按钮后的一系列行为都是事件处理器。
第二篇:学好事件,先学学观察者模式,挖根源,在进入事件前我们先学习了“观察者模式”,让你从原理上明白事件的流程,你学会了这种出镜率非常高的设计模式。
第三篇:一个活生生的Yii2事件例子,从一个实际有趣的例子引入,通过北哥和经理之间的博弈,让你在轻松中了解事件、学习yii事件常用方法、知道事件的运用场景等。
第四篇:学了两年半,Yii2的内置事件知多少?,yii是很贴心的,本篇为你讲解如何寻找yii提供的41种内置事件,以及具体如何去使用它们。
第五篇:处处蚊子咬,绑定事件知多少?,通过这篇我们知道了事件处理器的四种类型,以及事件阻断和事件排序,你可以应对很复杂的事件了。
Now
今天我们将学习事件的解绑和级别概念,准备开始,北哥和小x经理的对弈。
@@nai8@@
接上一篇
小x经理并没有停止对需求的探索,她和我说了这个“小事情”
- 她认为虽然烦死政府是我们的梦想,但是有一种情况要除外,就是我们开发人员登录的时候就不要发了,不但是政府,所有登陆后的事件都搞掉要,否则老板会找她谈话的。
通过对以上的学习,我们知道阻断事件是一种停止执行的方法,但是我必须要让这个阻断程序处于事件列表的第一位,这不是我想做的,因为上篇里我需要让Gov订阅者一直都前端。
没关系,我知道还有个和位置无关,且能干掉其他事件的方法,那就是解绑 。
在事件绑定之后,在触发之前,我们有机会做一些解绑工作。好吧,顺便复习下yii的事件解绑功能。
我贴一组代码,相信你一眼就能看明白解绑的使用方法。
$this->off(User::EVENT_AFTER_LOGIN, 'function_name');// 解绑一个已经绑定的全局事件处理器
$this->off(User::EVENT_AFTER_LOGIN, [$object, 'methodName']); // 解绑一个对象的方法
$this->off(User::EVENT_AFTER_LOGIN, ['app\components\Bar', 'methodName']); // 解绑一个类静态犯法
$this->off(User::EVENT_AFTER_LOGIN, $anonymousFunction); // 解绑一个匿名方法
$this->off(User::EVENT_AFTER_LOGIN); // 解绑所有绑定在此事件上的处理器
前三种我想你很容易就明白了,这里我说明下后两种
解绑一个匿名方法
我们知道可以为一个事件绑定一个匿名函数,例如
$this->on(User::EVENT_AFTER_LOGIN,function($event){
// 我是匿名函数,我骄傲
})
但是,我们不能通过
$this->off(User::EVENT_AFTER_LOGIN,function($event){
// 我是匿名函数,我骄傲
})
对其进行解绑,匿名函数就像内存中的一个叫做abc的值,可能在内存中存在两块都含abc的区域,但是如果我们不指定区域的名字,靠值是无法定位的,即便他们都是abc。
那怎么办那?我们可以将匿名函数放到一个变量中,然后再解绑此变量,这是唯一一种解绑匿名函数的方法。
$fuc = function($event){
// 我是匿名函数,我骄傲
};
$this->on(User::EVENT_AFTER_LOGIN,$fuc);
$this->off(User::EVENT_AFTER_LOGIN,$fuc);
只能这样。
解绑所有
这个很简单,执行一个只有事件名的解绑函数,毫不客气解绑之前绑定的所有。
说到这里,我想你和我一样兴奋了,因为小x经理的个需求我们已经找到了解决方案。
思路是这样的:判断当前登陆者是否是开发人员,如果是,在触发前解绑所有。
当然,对于我们的例子会有点复杂,因为我们触发的是yii内置的User类的一个事件。一种解决方法是我们新建一个该类的子类,然后在子类里对afterLogin方法进行重写,因我们主要将事件,就不再花篇幅细说这里。
ok,到此为止北哥和小x经理的故事也完结了。
事件级别
北哥和小x经理的故事覆盖了我们事件的大部分功能和使用场景,但是有一块我想还是需要给大家介绍下,那就是事件的级别。
事件的级别一共分三种
- 实例级别
- 类级别
- 全局级别
对于级别的阐述我决定复制网上广为流传的一段解释,复制的原因并非我认为他已经讲清楚了,而是北哥迄今还没有找到更贴切的例子将大家带入到级别的理解中。
如果你看懂下面的引用,那是最好的;如果没懂,没关系,在不用几天的时间内,我会单独出一篇针对于事件级别的故事,彻底解决对他们的理解。
我是引用分隔线----
实例级别也就是事件的触发、处理全部都在实例范围内。这种级别的事件用情专一,不与类的其他实例发生关系,也不与其他类、其他实例发生关系。除了实例级别的事件外,还有类级别的事件。对于Yii,由于Application是一个单例,所有的代码都可以访问这个单例。因此,有一个特殊级别的事件,全局事件。但是,本质上,他只是一个实例级别的事件。
这就好比是公司里的不同阶层。底层的码农们只能自己发发牢骚,个人的喜怒哀乐只发生在自己身上,影响不了其他人。而团队负责人如果心情不好,整个团队的所有成员今天都要战战兢兢,慎言慎行。到了公司老总那里,他今天不爽,哪个不长眼的敢上去触霉头?事件也是这样的,不同层次的事件,决定了他影响到的范围。
类级别事件
先讲讲类级别的事件。类级别事件用于响应所有类实例的事件。比如,工头需要了解所有工人的下班时间, 那么,对于数百个工人,即数百个Worker实例,工头难道要一个一个去绑定自己的handler么? 这也太低级了吧?其实,他只需要绑定一个handler到Worker类,这样每个工人下班时,他都能知道了。 与实例级别的事件不同,类级别事件的绑定需要使用 yii\base\Event::on()
Event::on(
Worker::className(), // 第一个参数表示事件发生的类
Worker::EVENT_OFF_DUTY, // 第二个参数表示是什么事件
function ($event) { // 对事件的处理
echo $event->sender . ' 下班了';
}
);
这样,每个工人下班时,会触发自己的事件处理函数,比如去打卡。之后,会触发类级别事件。 类级别事件的触发仍然是在 yii\base\Component::trigger() 中,还记得该函数的最后一个语句么:
Event::trigger($this, $name, $event); // 触发类一级的事件
这个语句就触发了类级别的事件。类级别事件,总是在实例事件后触发。既然触发时机靠后,那么如果有一天你要早退又不想老板知道,你就可以向小煤窑老板那样,通过 $event->handled = true ,来终止事件处理。
从 yii\base\Event::trigger() 的参数列表来看,比 yii\base\Component::trigger() 多了一个参数 $class 表示这是哪个类的事件。因此,在保存 $_event[] 数组上, yii\base\Event 也比 yii\base\Component 要多一个维度:
// Component中的$_event[] 数组
$_event[$eventName][] = [$handler, $data];
// Event中的$_event[] 数组
$_event[$eventName][$calssName][] = [$handler, $data];
那么,反过来的话,低级别的handler可以在高级别事件发生时发生作用么?这当然也是不行的。由于类级别事件不与任意的实例相关联,所以,类级别事件触发时,类的实例可能都还没有呢,怎么可能进行处理呢?
类级别事件的触发,应使用 yii\base\Event::trigger() 。这个函数不会触发实例级别的事件。值得注意的是, $event->sender 在实例级别事件中, $event->sender 指向触发事件的实例,而在类级别事件中, 指向的是类名。在 yii\base\Event::trigger() 代码中,有:
if (is_object($class)) { // $class 是trigger()的第一个参数,表示类名
if ($event->sender === null) {
$event->sender = $class;
}
$class = get_class($class); // 传入的是一个实例,则以类名替换之
} else {
$class = ltrim($class, '\\');
}
这段代码会对 $evnet->sender 进行设置,如果传入的时候,已经指定了他的值,那么这个值会保留,否则,就会替换成类名。
对于类级别事件,有一个要格外注意的地方,就是他不光会触发自身这个类的事件,这个类的所有祖先类的同一事件也会被触发。但是,自身类事件与所有祖先类的事件,视为同一级别:
// 最外面的循环遍历所有祖先类
do {
if (!empty(self::$_events[$name][$class])) {
foreach (self::$_events[$name][$class] as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// 所有的事件都是同一级别,可以随时终止
if ($event->handled) {
return;
}
}
}
} while (($class = get_parent_class($class)) !== false);
上面的嵌套循环的深度,或者叫时间复杂度,受两个方面影响,一是类继承结构的深度,二是 $_event[$name][$class][] 数组的元素个数,即已经绑定的handler的数量。从实践经验看,一般软件工程继承深度超过十层的就很少见,而事件绑定上,同一事件的绑定handler超过十几个也比较少见。因此,上面的嵌套循环运算数量级大约在100~1000之间,这是可以接受的。
但是,从机制上来讲,由于类级别事件会被类自身、类的实例、后代类、后代类实例所触发,所以,对于越底层的类而言,其类事件的影响范围就越大。因此,在使用类事件上要注意,尽可能往后代类安排,以控制好影响范围,尽可能不在基础类上安排类事件。
全局事件
接下来再讲讲全局级别事件。上面提到过,所谓的全局事件,本质上只是一个实例事件罢了。他只是利用了Application实例在整个应用的生命周期中全局可访问的特性,来实现这个全局事件的。当然,你也可以将他绑定在任意全局可访问的的Component上。
全局事件一个最大优势在于:在任意需要的时候,都可以触发全局事件,也可以在任意必要的时候绑定,或解除一个事件:
Yii::$app->on('bar', function ($event) {
echo get_class($event->sender); // 显示当前触发事件的对象的类名称
});
Yii::$app->trigger('bar', new Event(['sender' => $this]));
上面的 Yii::$app->on() 可以在任何地方调用,就可以完成事件的绑定。而 Yii::$app->trigger() 只要在绑定之后的任何时候调用就OK了。
^^^ 引用结束 ^^^
最后要说的话
匆匆6篇文章,感谢大家的阅读,当然仅仅6篇文章不足以覆盖“事件机制”的所有,但至少为大家起了一个敢用事件的头儿,多用多思考,这是我们理解一件事情的唯一法门。
后期对于事件的分享也会放到此专题下,但是不会像这几天一样,大篇幅,集中突击。
接下来我们要研究yii的行为模式和一些实际技法专题,比如有个专题叫“yii的所有第三方登陆”,“yii的所有支付”等等。感谢大家对工兵连的支持。
共 3 条回复
-
写的很好!最近也在写关于Yii2的系列文章,解剖Yii2实现的原理,附带设计模式/设计原则的点评。这是事件的部分:
http://www.yiichina.com/tutorial/1611
愿相互学习,相互进步!凉凉的晴冷天 , HongXunPan 觉得很赞
abei1982 河南洛阳
最后登录:2020-04-14
在线时长:128小时48分
- 粉丝307
- 金钱4935
- 威望50
- 积分6715