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