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阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,046评论 0 29
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,857评论 25 707
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,934评论 6 13
  • 我学英语的兴趣是从哪里开始的呢? 小学的时候,大我四岁的姐姐在上英语补习班,每天戴着随身听叽里呱啦说些我听不懂的语...
    Miss_koala阅读 948评论 0 0