yii2框架源碼分析系列(6)之事件

回顧

上一篇聊了下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ī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贞言,一起剝皮案震驚了整個(gè)濱河市斜棚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌该窗,老刑警劉巖弟蚀,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酗失,居然都是意外死亡义钉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門规肴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捶闸,“玉大人,你說我怎么就攤上這事拖刃∩咀常” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵兑牡,是天一觀的道長央碟。 經(jīng)常有香客問我,道長均函,這世上最難降的妖魔是什么亿虽? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮边酒,結(jié)果婚禮上经柴,老公的妹妹穿的比我還像新娘狸窘。我一直安慰自己墩朦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布翻擒。 她就那樣靜靜地躺著氓涣,像睡著了一般牛哺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劳吠,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天引润,我揣著相機(jī)與錄音,去河邊找鬼痒玩。 笑死淳附,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蠢古。 我是一名探鬼主播奴曙,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼草讶!你這毒婦竟也來了洽糟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤堕战,失蹤者是張志新(化名)和其女友劉穎坤溃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘱丢,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薪介,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了越驻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昭灵。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伐谈,靈堂內(nèi)的尸體忽然破棺而出烂完,到底是詐尸還是另有隱情,我是刑警寧澤诵棵,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布抠蚣,位于F島的核電站,受9級特大地震影響履澳,放射性物質(zhì)發(fā)生泄漏嘶窄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一距贷、第九天 我趴在偏房一處隱蔽的房頂上張望柄冲。 院中可真熱鬧,春花似錦忠蝗、人聲如沸现横。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戒祠。三九已至骇两,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間姜盈,已是汗流浹背低千。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留馏颂,地道東北人示血。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像救拉,于是被迫代替她去往敵國和親矾芙。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理近上,服務(wù)發(fā)現(xiàn)剔宪,斷路器,智...
    卡卡羅2017閱讀 134,661評論 18 139
  • ??JavaScript 與 HTML 之間的交互是通過事件實(shí)現(xiàn)的。 ??事件斗锭,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,495評論 1 11
  • 這些天地淀,南方地區(qū)持續(xù)的升溫,無論是空間還是朋友圈都被高溫給刷屏了岖是。見到一個(gè)人帮毁,閑聊的第一句話竟是“這天熱得……”取...
    心中住了一棵樹閱讀 314評論 0 0
  • 評論《賈老師》 我寫的《賈老師》小文,只是記敘了一個(gè)鄉(xiāng)村老教師的一個(gè)簡單的側(cè)面豺撑,卻收到了人們...
    邯鄲趙金海閱讀 372評論 0 0
  • 十五元宵鬧人圓 古人云:“千門開鎖萬燈明,正月中旬動(dòng)地京”陆错。眾所周知灯抛,正月十五是元宵節(jié),又稱上元節(jié)音瓷、元夜对嚼、春燈節(jié)等...
    IDO老徐閱讀 1,702評論 1 17