abei1982 2017-05-25 23:34:30 4615次浏览 3条回复 9 6 0

感谢你坚持读到这里,本篇是“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的所有支付”等等。感谢大家对工兵连的支持。

觉得很赞
您需要登录后才可以回复。登录 | 立即注册