回顧
上一篇聊了下yii2的Application
源梭,本來這篇應(yīng)該繼續(xù)后面的url解析了伊磺,但是有些前置知識還是需要提前解釋斧拍,所以今天來介紹下yii2中的事件Event
Event
事件是yii2中一個(gè)非常重要的特性蜀备,可以很好的實(shí)現(xiàn)代碼解耦关摇,同時(shí)也是一種流行的任務(wù)流程設(shè)計(jì)模式 ,我們在業(yè)務(wù)處理中碾阁,都會碰到針對某個(gè)觸發(fā)點(diǎn)而執(zhí)行一個(gè)或多個(gè)事件的情況输虱,而某些事件又可以埋到多個(gè)觸發(fā)點(diǎn),實(shí)現(xiàn)代碼復(fù)用
實(shí)現(xiàn)
yii2中事件的實(shí)現(xiàn)主要通過Component
類和Event
類來實(shí)現(xiàn)瓷蛙,Event
類是所有事件的基類悼瓮,其中囊括了事件所需的參數(shù)和方法戈毒,直接看代碼
class Event extends BaseObject
{
// 事件名
public $name;
// 事件發(fā)布者
public $sender;
// 是否終止后續(xù)事件的執(zhí)行,默認(rèn)不終止
public $handled = false;
// 事件相關(guān)數(shù)據(jù)
public $data;
// 全局記錄已注冊事件
private static $_events = [];
// 全局記錄已注冊通配符模式事件
private static $_eventWildcards = [];
// 綁定類級別事件handler
public static function on($class, $name, $handler, $data = null, $append = true)
{
$class = ltrim($class, '\\');
// 類名或者事件名中有通配符横堡,走通配符模式
if (strpos($class, '*') !== false || strpos($name, '*') !== false) {
if ($append || empty(self::$_eventWildcards[$name][$class])) {
// 尾部添加到_eventWildcards靜態(tài)數(shù)組中
self::$_eventWildcards[$name][$class][] = [$handler, $data];
} else {
// 頭部添加到_eventWildcards靜態(tài)數(shù)組中
array_unshift(self::$_eventWildcards[$name][$class], [$handler, $data]);
}
return;
}
if ($append || empty(self::$_events[$name][$class])) {
// 尾部添加到_events靜態(tài)數(shù)組中
self::$_events[$name][$class][] = [$handler, $data];
} else {
// 頭部添加到_events靜態(tài)數(shù)組中
array_unshift(self::$_events[$name][$class], [$handler, $data]);
}
}
// 解綁類級別事件handler
public static function off($class, $name, $handler = null)
{
$class = ltrim($class, '\\');
if (empty(self::$_events[$name][$class]) && empty(self::$_eventWildcards[$name][$class])) {
// 本來就沒有綁定埋市,直接返回false
return false;
}
// 解綁所有handler
if ($handler === null) {
// 完全匹配模式解綁
unset(self::$_events[$name][$class]);
// 通配符模式解綁
unset(self::$_eventWildcards[$name][$class]);
return true;
}
// 解綁指定handler,完全匹配模式
if (isset(self::$_events[$name][$class])) {
$removed = false;
foreach (self::$_events[$name][$class] as $i => $event) {
if ($event[0] === $handler) {
// 找到指定的handler并解綁
unset(self::$_events[$name][$class][$i]);
// 設(shè)置解綁標(biāo)識
$removed = true;
}
}
if ($removed) {
// 重新索引
// 因?yàn)槭菙?shù)字索引命贴,unset會造成索引值跳躍
self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
return $removed;
}
}
// 解綁指定handler道宅,通配符匹配模式
$removed = false;
foreach (self::$_eventWildcards[$name][$class] as $i => $event) {
if ($event[0] === $handler) {
// 找到指定的handler并解綁
unset(self::$_eventWildcards[$name][$class][$i]);
// 設(shè)置解綁標(biāo)識
$removed = true;
}
}
if ($removed) {
// 重新索引
self::$_eventWildcards[$name][$class] = array_values(self::$_eventWildcards[$name][$class]);
// 解綁之后處理掉空的數(shù)組元素
// 這么做主要是為了減少后續(xù)正則匹配的消耗
if (empty(self::$_eventWildcards[$name][$class])) {
unset(self::$_eventWildcards[$name][$class]);
if (empty(self::$_eventWildcards[$name])) {
unset(self::$_eventWildcards[$name]);
}
}
}
return $removed;
}
// 解綁所有類級別事件handler
public static function offAll()
{
// 直接全部置空
self::$_events = [];
self::$_eventWildcards = [];
}
// 判斷是否
public static function hasHandlers($class, $name)
{
if (empty(self::$_eventWildcards) && empty(self::$_events[$name])) {
// 沒綁定過,那絕對不存在了
// 這里判斷的是_eventWildcards而不是_eventWildcards[$name]胸蛛,主要是因?yàn)樵摂?shù)組是以通配符模式保存的污茵,并不能直接定位到$name
return false;
}
if (is_object($class)) {
// 獲取對象名
$class = get_class($class);
} else {
$class = ltrim($class, '\\');
}
// 這里需要說明下
// 子類繼承父類會同時(shí)會擁有父類綁定的事件
// 類實(shí)現(xiàn)接口也會擁有接口綁定的事件
// 所以你要找某個(gè)類某個(gè)事件是否綁定了handler,也需要判斷其繼承的所有父類和實(shí)現(xiàn)的所有接口
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);
// 完全匹配模式下查找綁定
foreach ($classes as $class) {
if (!empty(self::$_events[$name][$class])) {
return true;
}
}
// 通配符匹配模式下查找綁定
foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
// 這里使用yii2的Helper類中的方法葬项,用于匹配通配符模式泞当,這里不贅述該方法,有興趣可以自己查閱
// 先找到匹配的事件名name
if (!StringHelper::matchWildcard($nameWildcard, $name)) {
continue;
}
// 再匹配類名class
foreach ($classHandlers as $classWildcard => $handlers) {
if (empty($handlers)) {
// 沒有綁定handler民珍,直接跳過
continue;
}
foreach ($classes as $class) {
// 循環(huán)匹配所有類名襟士,父類名,接口名
// 這里使用了!嚷量,仔細(xì)看matchWildcard這個(gè)方法了陋桂,里面會把\*這樣的格式轉(zhuǎn)換成[*]來處理,這個(gè)是有問題的蝶溶,具體什么問題嗜历,感興趣的可以評論里交流,這里就不說了抖所,盡量不要濫用通配符匹配模式
if (!StringHelper::matchWildcard($classWildcard, $class)) {
return true;
}
}
}
}
return false;
}
// 觸發(fā)類級別事件
public static function trigger($class, $name, $event = null)
{
$wildcardEventHandlers = [];
// 獲取通配符模式下匹配name的所有handler
foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
if (!StringHelper::matchWildcard($nameWildcard, $name)) {
continue;
}
$wildcardEventHandlers = array_merge($wildcardEventHandlers, $classHandlers);
}
// 沒有對應(yīng)的handler梨州,無法執(zhí)行,直接返回
if (empty(self::$_events[$name]) && empty($wildcardEventHandlers)) {
return;
}
// 這里注意到trigger方法的第三個(gè)參數(shù)event應(yīng)該是一個(gè)event類實(shí)例部蛇,主要是通過event類中的private屬性來統(tǒng)一規(guī)范傳遞給handler的參數(shù)
if ($event === null) {
// 沒有就自己造一個(gè)摊唇,這里用了延遲綁定咐蝇,可用于子類調(diào)用
$event = new static();
}
// 一些初始化
$event->handled = false;
$event->name = $name;
if (is_object($class)) {
if ($event->sender === null) {
// 如果傳的是對象涯鲁,并且sender=null,直接將對象賦值給sender
$event->sender = $class;
}
$class = get_class($class);
} else {
$class = ltrim($class, '\\');
}
// 老規(guī)矩有序,組裝本類名抹腿,父類名,接口名
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);
foreach ($classes as $class) {
// 單次循環(huán)要執(zhí)行的handler
// 也就是按照類的層級旭寿,從子類到父類再到接口逐步執(zhí)行對應(yīng)的handler
$eventHandlers = [];
foreach ($wildcardEventHandlers as $classWildcard => $handlers) {
if (StringHelper::matchWildcard($classWildcard, $class)) {
// 收集通配符模式下匹配到的handler
$eventHandlers = array_merge($eventHandlers, $handlers);
// 這里每次匹配到一次之后應(yīng)該unset掉警绩,因?yàn)榭赡軙?dǎo)致重復(fù)執(zhí)行
// 這里需要注意下,因?yàn)楦割惡妥宇惪赡苁褂玫耐粋€(gè)匹配模式盅称,所以肩祥,父類和子類的handler可能是交叉執(zhí)行的后室,并不是按照層級遞增來調(diào)用
unset($wildcardEventHandlers[$classWildcard]);
}
}
// 收集完全匹配模式下的handler
if (!empty(self::$_events[$name][$class])) {
// 這里就不用unset了,因?yàn)榫哂形ㄒ恍? $eventHandlers = array_merge($eventHandlers, self::$_events[$name][$class]);
}
// 執(zhí)行單次循環(huán)收集到的handler混狠,并把組裝的event實(shí)例作為參數(shù)傳進(jìn)去
foreach ($eventHandlers as $handler) {
// 初始化data岸霹,這個(gè)data就是on()方法在綁定的時(shí)候定義的
$event->data = $handler[1];
// 這里是真正執(zhí)行的位置
// 不用我說你也知道吧,on()傳入的handler的格式得遵循call_user_func的規(guī)則
call_user_func($handler[0], $event);
if ($event->handled) {
// 執(zhí)行終止
// 這里傳的是對象event将饺,我們都知道event作為參數(shù)傳遞是引用傳遞贡避,所以任何一個(gè)handler都可以在執(zhí)行過程中將event的handled置為true,終止后面的handler予弧,這也是為什么要硬性規(guī)定傳遞event對象作為參數(shù)的理由吧
return;
}
}
}
}
}
上面的代碼中舊版是沒有通配符匹配模式的刮吧,在2.0.14中加入的,但是我覺得這個(gè)沒有實(shí)現(xiàn)好掖蛤,上面注釋有說明杀捻,而且從允許handler執(zhí)行終止來看,handler的執(zhí)行順序是很重要的蚓庭,但是通配符模式中并沒有把控好這個(gè)順序水醋,甚至在hasHandlers()
方法里面都有明顯的錯(cuò)誤,Event
的實(shí)現(xiàn)大概就是這樣彪置,它包含了通用的靜態(tài)方法和靜態(tài)屬性拄踪,來執(zhí)行和保存全局,同時(shí)也包含私有屬性拳魁,來規(guī)范并傳遞參數(shù)
Component
Event
類繼承的是基類BaseObject
惶桐,貌似看著跟框架主體沒有一點(diǎn)聯(lián)系啊,那是因?yàn)?code>Event默認(rèn)是類級別的事件潘懊,正確的使用方式是你自己對你的業(yè)務(wù)涉及一些事件類姚糊,這些類都繼承自Event
,每個(gè)自定義事件類有自己獨(dú)特的屬性授舟,這樣就可以區(qū)分不同種類的事件了救恨,那框架主體怎么使用事件機(jī)制的呢,這里就需要看看Component
類了释树,Component
實(shí)現(xiàn)的是全局級別的事件肠槽,會貫穿整個(gè)生命周期
// Component中對應(yīng)處理事件的幾個(gè)方法
// 綁定全局事件handler
// 既然是全局的,就需要傳class了咯
public function on($name, $handler, $data = null, $append = true)
{
// 這個(gè)先別管奢啥,行為范疇秸仙,后面會講
$this->ensureBehaviors();
// 同樣區(qū)分匹配模式
// 大致行為跟Event類差不多,只是保存handler的數(shù)組里少了class這一維度桩盲,并且保存handler的數(shù)組不再是static寂纪,而是private,因?yàn)槲覀兺ㄟ^入口創(chuàng)建的Application實(shí)例會繼承到Component,所以屬于單個(gè)實(shí)例私有的數(shù)據(jù)
if (strpos($name, '*') !== false) {
if ($append || empty($this->_eventWildcards[$name])) {
$this->_eventWildcards[$name][] = [$handler, $data];
} else {
array_unshift($this->_eventWildcards[$name], [$handler, $data]);
}
return;
}
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
array_unshift($this->_events[$name], [$handler, $data]);
}
}
// 解除全局事件綁定
public function off($name, $handler = null)
{
$this->ensureBehaviors();
// 沒有綁定過捞蛋,解除個(gè)毛線
if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
return false;
}
// 未指定handler孝冒,全解除
if ($handler === null) {
unset($this->_events[$name], $this->_eventWildcards[$name]);
return true;
}
$removed = false;
// plain event names
if (isset($this->_events[$name])) {
// 指定handler,邏輯與Event類中的off一樣
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
return $removed;
}
}
// 通配符模式拟杉,邏輯與Event類中的off一樣
if (isset($this->_eventWildcards[$name])) {
foreach ($this->_eventWildcards[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_eventWildcards[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
// remove empty wildcards to save future redundant regex checks:
if (empty($this->_eventWildcards[$name])) {
unset($this->_eventWildcards[$name]);
}
}
}
return $removed;
}
// 觸發(fā)執(zhí)行事件綁定的handler
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();
$eventHandlers = [];
// 沒有類這一層級了迈倍,就不需要循環(huán)調(diào)用了,直接收集所有符合的handler
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (StringHelper::matchWildcard($wildcard, $name)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}
if (!empty($this->_events[$name])) {
// 這里和Event類差不多
// 除了在設(shè)置sender上面捣域,這里把this賦值給sender啼染,表示當(dāng)前實(shí)例
$eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
}
if (!empty($eventHandlers)) {
if ($event === null) {
$event = new Event();
}
if ($event->sender === null) {
$event->sender = $this;
}
$event->handled = false;
$event->name = $name;
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
if ($event->handled) {
return;
}
}
}
// 這里還會觸發(fā)類級別的事件handler
Event::trigger($this, $name, $event);
}
上面就是Component
中實(shí)現(xiàn)的全局事件機(jī)制,只跟當(dāng)前Application
實(shí)例綁定焕梅,而Application
通過注冊Yii::$app
來實(shí)現(xiàn)全局可訪問迹鹅,這樣整個(gè)生命周期都可以執(zhí)行全局事件的綁定、解綁和觸發(fā)執(zhí)行機(jī)制