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類
這個類已經多次接觸到,總結這個類的使用場景,發現他主要有兩個用途:
- 用于向事件處理函數傳遞信息。
- 用于觸發類事件。
之前說的事件的綁定,解除的操作,都是基于某一個實例化的對象來說的,假如說某一個類被實例化出來了好多對象,現在想對所有的對象都綁定某一個事件,那就需要對這些對象依次進行綁定,這樣做豈不是很麻煩,這時候就可以使用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;
}
}