Yii2框架源碼研究2-Component

Component繼承自Object,因此他具有屬性這個特性,在這個基礎上,組件提供了兩個功能強大的特性:事件和行為。也就是說,如果一個類繼承了Component類,他就具有這些特性,就能夠給這個類的對象綁定事件和行為。

事件的作用是在某一個特殊的場合,執行某段代碼。一個事件通常包含以下幾個要素:

  • 這是一個什么事件
  • 誰觸發了事件
  • 誰去處理事件
  • 怎么處理這個事件
  • 處理事件相關的數據是什么

行為的作用是讓某一個對象擁有某一些方法和屬性,這些方法和屬性被封裝在一個行為里,當這個行為依附在某個類中的時候,這個類就具有了這個行為提供的屬性和方法。


為了理解組件是怎么實現這兩個特性的,首先需要看一下Component的源代碼

class Component extends Object
{
    private $_events = [];
    private $_behaviors;
    public function __get($name)
    public function __set($name, $value)
    public function __isset($name)
    public function __unset($name)
    public function __call($name, $params)
    public function __clone()
    {
        $this->_events = [];
        $this->_behaviors = null;
    }
    public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
    public function hasMethod($name, $checkBehaviors = true)
    public function behaviors()
    {
        return [];
    }
    public function hasEventHandlers($name)
    public function on($name, $handler, $data = null, $append = true)
    public function off($name, $handler = null)
    public function trigger($name, Event $event = null)
    public function getBehavior($name)
    public function getBehaviors()
    public function attachBehavior($name, $behavior)
    public function attachBehaviors($behaviors)
    public function detachBehavior($name)
    public function detachBehaviors()
    public function ensureBehaviors()
    private function attachBehaviorInternal($name, $behavior)
}

咋一看,發現Component將Object類中的方法全都重寫了,好吧。那就先來看看屬性這個特性。

屬性

Component類沒有構造方法,因此其初始化的過程和Object類是一樣的,對屬性的操作也都會定位到魔術方法__set()或者__get()里面,一個一個看:

    public function __set($name, $value)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter)) {
            $this->$setter($value);
            return;
        } elseif (strncmp($name, 'on ', 3) === 0) {
            $this->on(trim(substr($name, 3)), $value);
            return;
        } elseif (strncmp($name, 'as ', 3) === 0) {
            $name = trim(substr($name, 3));
            $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
            return;
        } else {
            $this->ensureBehaviors();
            foreach ($this->_behaviors as $behavior) {
                if ($behavior->canSetProperty($name)) {
                    $behavior->$name = $value;
                    return;
                }
            }
        }
        if (method_exists($this, 'get' . $name)) {
            throw 
        } else {
            throw 
        }
    }

一目了然,為什么要重寫這個方法,因為component的配置數組中可以配置事件和行為,on+空格表示事件,as+空格表示行為。由于行為的屬性也是組件的屬性,因此還會去行為中查找相應的屬性。

    public function __get($name)
    {
        $getter = 'get' . $name;
        if (method_exists($this, $getter)) {
            // read property, e.g. getName()
            return $this->$getter();
        } else {
            // behavior property
            $this->ensureBehaviors();
            foreach ($this->_behaviors as $behavior) {
                if ($behavior->canGetProperty($name)) {
                    return $behavior->$name;
                }
            }
        }
        if (method_exists($this, 'set' . $name)) {
            throw
        } else {
            throw 
        }
    }

__get()函數中會去行為中尋找相應的屬性。

事件

開頭說了事件的基本概念,現在來看一下具體有哪些方法吧。
  首先在component類中,定義了一個數組用來存儲所有的事件:

    private $_events = [];//name => handlers

這里的handlers是一個數組,因為有可能一個事件有許多事件處理函數。數組里每一個項都是[$handler, $data] 的結構,其中,$handler的結構如下:

      function ($event) { ... }         // anonymous function
      [$object, 'handleClick']          // $object->handleClick()
      ['Page', 'handleClick']           // Page::handleClick()
      'handleClick'                     // global function handleClick()

為什么是這四種呢?后面會看到,在trigger函數中調用了call_user_func函數,這個函數允許使用這四種方式去執行一個方法。

事件綁定與解除

綁定事件所進行的操作是將事件的名稱和事件處理函數對應起來,并將這個對應關系放在event數組里面。方法如下:

    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]);
        }
    }

相應的事件的解除也就是將事件與其處理函數的關系在event數組中移除。如果$handler為空,將會刪除這個事件的所有時間處理函數,相應的函數如下:

    public function off($name, $handler = null)
    {
        $this->ensureBehaviors();
        if (empty($this->_events[$name])) {
            return false;
        }
        if ($handler === null) {
            unset($this->_events[$name]);
            return true;
        } else {
            $removed = false;
            foreach ($this->_events[$name] as $i => $event) {
                if ($event[0] === $handler) {
                    unset($this->_events[$name][$i]);
                    $removed = true;
                }
            }
            if ($removed) {
                //因為unset之后,key混亂了,這樣的話key就不混亂了
                $this->_events[$name] = array_values($this->_events[$name]);
            }
            return $removed;
        }
    }

事件觸發

事件觸發后發生的事情就是執行所有綁定的事件處理函數,具體到操作上來說就是遍歷數組event[$name],將數據傳遞給事件handler并執行。

    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);
    }

這里需要注意的一個地方是,在循環執行所有的事件處理函數的時候,如果某個handler將$event->handled置為true,那么剩下的handler將不會被執行。Event::trigger()這個函數用于觸發類事件。

Event類

這個類已經多次接觸到,總結這個類的使用場景,發現他主要有兩個用途:

  1. 用于向事件處理函數傳遞信息。
  2. 用于觸發類事件。

之前說的事件的綁定,解除的操作,都是基于某一個實例化的對象來說的,假如說某一個類被實例化出來了好多對象,現在想對所有的對象都綁定某一個事件,那就需要對這些對象依次進行綁定,這樣做豈不是很麻煩,這時候就可以使用Event類提供的機制,綁定一個類事件,所有從這個類實例化出來的對象都能夠觸發這個事件。現在來看一下Event類的代碼:

class Event extends Object
{
    public $name;
    public $sender;
    public $handled = false;
    public $data;
    private static $_events = [];
    public static function on($class, $name, $handler, $data = null, $append = true)
    {
        $class = ltrim($class, '\\');
        if ($append || empty(self::$_events[$name][$class])) {
            self::$_events[$name][$class][] = [$handler, $data];
        } else {
            array_unshift(self::$_events[$name][$class], [$handler, $data]);
        }
    }
    public static function off($class, $name, $handler = null)
    {
        $class = ltrim($class, '\\');
        if (empty(self::$_events[$name][$class])) {
            return false;
        }
        if ($handler === null) {
            unset(self::$_events[$name][$class]);
            return true;
        } else {
            $removed = false;
            foreach (self::$_events[$name][$class] as $i => $event) {
                if ($event[0] === $handler) {
                    unset(self::$_events[$name][$class][$i]);
                    $removed = true;
                }
            }
            if ($removed) {
                self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
            }

            return $removed;
        }
    }
    public static function hasHandlers($class, $name)
    {
        if (empty(self::$_events[$name])) {
            return false;
        }
        if (is_object($class)) {
            $class = get_class($class);
        } else {
            $class = ltrim($class, '\\');
        }
        do {
            if (!empty(self::$_events[$name][$class])) {
                return true;
            }
        } while (($class = get_parent_class($class)) !== false);

        return false;
    }
    public static function trigger($class, $name, $event = null)
    {
        if (empty(self::$_events[$name])) {
            return;
        }
        if ($event === null) {
            $event = new static;
        }
        $event->handled = false;
        $event->name = $name;

        if (is_object($class)) {
            if ($event->sender === null) {
                $event->sender = $class;
            }
            $class = get_class($class);
        } else {
            $class = ltrim($class, '\\');
        }
        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類中同樣有一個$_events數組,里面保存的內容和Component里面的內容一樣,只不過,由于需要根據類名來尋找相應的類事件,因此現在的數組中多了一層:$_events[$name][$class][] = [$handler, $data];
  注冊類事件:

Event::on(  Worker::className(),               // 第一個參數表示事件發生的類 
            Worker::EVENT_OFF_DUTY,            // 第二個參數表示是什么事件 
            function ($event) {                // 對事件的處理 
                echo $event->sender . ' 下班了'; 
            }
);

觸發類事件,這里$this的作用僅僅是需要知道是誰觸發的事件,然后根據這個對象得到其類的名稱:

Event::trigger($this, $name, $event); 

行為

行為是一個類,想要新建一個行為,首先需要新建一個繼承yii\base\Behavior 的類,然后將這個行為依附到另外一個繼承了Component或其子類的類上,這個類就有了這個行為,就有了這個行為所具有的屬性和方法。依附的過程就是調用這個類的attach方法,相應的解綁的過程就是調用其detach方法。綁定的時候會將行為的事件注冊到擁有者,這個擁有者一定是一個Component。先來看看Behavior類:

class Behavior extends Object
{
    public $owner;


    /**
     * Declares event handlers for the [[owner]]'s events.
     * - method in this behavior: `'handleClick'`, equivalent to `[$this, 'handleClick']`
     * - object method: `[$object, 'handleClick']`
     * - static method: `['Page', 'handleClick']`
     * - anonymous function: `function ($event) { ... }`
    */
    public function events()
    {
        return [];
    }
    public function attach($owner)
    {
        $this->owner = $owner;
        foreach ($this->events() as $event => $handler) {
            $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
        }
    }
    public function detach()
    {
        if ($this->owner) {
            foreach ($this->events() as $event => $handler) {
                $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
            }
            $this->owner = null;
        }
    }
}

組件對行為的控制

組件中有一個變量$_behaviors用于存儲所有的行為,這是一個數組(behavior name => behavior)并且這里的behavior表示一個類,當$_behaviors為null的時候,說明還沒有初始化。

    public function ensureBehaviors()
    {
        if ($this->_behaviors === null) {
            $this->_behaviors = [];
            foreach ($this->behaviors() as $name => $behavior) {
                $this->attachBehaviorInternal($name, $behavior);
            }
        }
    }

這個函數剛剛已經碰到過,就是初始化所有行為的一個過程,首先調用函數得到所有的行為,然后依次執行函數attachBehaviorInternal。
  有一點需要說明,在__set()函數中,對as+空格的屬性進行了特殊處理,將其當做一個行為來看,這時候調用了attachBehavior函數對這個行為進行attach的處理,在這個函數中首先調用了ensureBehaviors,也就是首先要初始化behaviors()函數定義的行為。相同名稱的行為出現時,后者會覆蓋前者,因此在配置數組里配置的行為的優先級高于behaviors()函數定義的行為。

行為attach過程

attach的行為一共有兩種來源,一種是配置數組中利用as+空格定義的,一種是在behaviors()函數中返回的,最終都會調用一個函數:

    private function attachBehaviorInternal($name, $behavior)
    {
        if (!($behavior instanceof Behavior)) {
            $behavior = Yii::createObject($behavior);
        }
        if (is_int($name)) {
            $behavior->attach($this);
            $this->_behaviors[] = $behavior;
        } else {
            if (isset($this->_behaviors[$name])) {
                $this->_behaviors[$name]->detach();
            }
            $behavior->attach($this);
            $this->_behaviors[$name] = $behavior;
        }
        return $behavior;
    }

如果一個行為的name是一個整數,那么這個行為僅僅是知性了這個行為的attach函數,其屬性和方法并未依附到主體上來。依附的過程就是首先將behavior實例化,然后將其賦值給_behaviors數組,如果已存在同名的行為,則覆蓋。

detach過程

主要有兩步,將$behavior對象從$_behavior中移除,調用$behavior的detach()方法

    public function detachBehavior($name)
    {
        $this->ensureBehaviors();
        if (isset($this->_behaviors[$name])) {
            $behavior = $this->_behaviors[$name];
            unset($this->_behaviors[$name]);
            $behavior->detach();
            return $behavior;
        } else {
            return null;
        }
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,070評論 0 29
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,769評論 25 708
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,079評論 6 13
  • 我學英語的興趣是從哪里開始的呢? 小學的時候,大我四歲的姐姐在上英語補習班,每天戴著隨身聽嘰里呱啦說些我聽不懂的語...
    Miss_koala閱讀 967評論 0 0