资讯专栏INFORMATION COLUMN

浅析Yii2.0的行为Behavior

legendmohe / 2017人阅读

摘要:概念理解使用行为可以在不修改现有类的情况下,对类的功能进行扩充。最后将行为名称和行为实例放到的属性中,至此,行为的绑定就结束了。不过在解除的时候虽然都是删掉相应的,但是解除行为还需要解除在绑定行为的时候绑定的事件,这点不太一样。

概念理解:使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。 通过将行为绑定到一个类,可以使类具有行为本身所定义的属性和方法,就好像类本来就有这些属性和方法一样。 而不需要写一个新的类去继承或包含现有类。在功能上类似于 Traits ,达到类似于多继承的目的。
行为的实现demo
 "afterAttach"
        ];
    }

    public function afterAttach()
    {
        echo "事件已触发";
    }
}

// 在控制器或者命令行下调用

$myClass = new MyClass();

$myBehavior = new BehaviorTest();

// 将行为绑定到 MyClass 的实例
$myClass->attachBehavior("test", $myBehavior);

// MyClass 实例调用行为类中的属性
echo $myClass->_val;

// MyClass 实例调用行为类中的方法
$myClass->getOutput();

// MyClass 实例触发行为类中定义的事件
$myClass->trigger(BehaviorTest::EVENT_AFTER_SAVE);
行为的绑定原理

我们先来看一下 $myClass->attachBehavior("test", $myBehavior); 行为绑定的时候做了哪些事情?好的,又是我们的老朋友yiiaseComponent

// yiiaseComponent 的部分代码

private $_behaviors;

public function behaviors()
{
    return [];
}

public function attachBehavior($name, $behavior)
{
        $this->ensureBehaviors();
        return $this->attachBehaviorInternal($name, $behavior);
}

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

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

$myClass 调用 attachBehavior() 传入俩个参数,一个是行为名称,另一个是行为类的名称或实例或者是数组,接着 attachBehavior() 调用了 ensureBehaviors(),这个函数我们暂时用不到,因为我们没有在MyClass里面重载behaviors(),不过也能大概猜到ensureBehaviors()的用途了。再往下调用的是私有函数attachBehaviorInternal(),这个函数先判断传进来的$behavior 是否已实例化,如果还没有则进行实例化,再通过$name判断是匿名行为还是命名行为,如果是命名行为,需要查看是否已经绑定同名的行为,如果绑定了同名的行为,会将以前的行为先解绑再调用$behavior->attach($this);[注:这里$this指的MyClass实例,也就是$myClass],这样我们就来到了yiiaseBehaviorattach()方法,下面是attach()方法的源码:

public function attach($owner)
{
    $this->owner = $owner;
    foreach ($this->events() as $event => $handler) {
        $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}

$this->owner = $owner;[注:这里$this 指的是$behavior也就是类BehaviorTest的实例],这句代码指定了行为类的主人是谁,后面的代码看着似乎似曾相识?是的,就是将行为类events()方法里面的事件也绑定的宿主身上,这里不具体展开,有兴趣的小伙伴可以看一下浅析Yii2.0的事件Event。最后将行为名称和行为实例放到$myClass的属性_behavior中,至此,行为的绑定就结束了。好像也没干什么啊,我们现在可以打印一下$myClass的数据结构是怎样的?

commoncomponentsMyClass Object
(
    [_events:yiiaseComponent:private] => Array
        (
            [eventAfterAttach] => Array
                (
                    [0] => Array
                        (
                            [0] => Array
                                (
                                    [0] => commoncomponentsBehaviorTest Object
                                        (
                                            [_val] => 我是BehaviorTest里面的公有属性_val
                                            [owner] => commoncomponentsMyClass Object
 *RECURSION*
                                        )

                                    [1] => afterAttach
                                )

                            [1] => 
                        )

                )

        )

    [_eventWildcards:yiiaseComponent:private] => Array
        (
        )

    [_behaviors:yiiaseComponent:private] => Array
        (
            [test] => commoncomponentsBehaviorTest Object
                (
                    [_val] => 我是BehaviorTest里面的公有属性_val                   
                    [owner] => commoncomponentsMyClass Object
 *RECURSION*
                )

        )

)

可以看到$myClass已经绑定了一个行为test,绑定了一个事件eventAfterAttach,那么绑定行为以后,是怎么调用行为类里面的属性和方法呢?

行为的使用原理

还是看一下demo里面$myClass->_val这句代码是怎么执行的?根据上面$myClass的数据结构可以看出并没有_val这个属性,但是yiiaseComponent里面实现了__get()这个魔术方法,我们看一下源码。

public function __get($name)
{
    $getter = "get" . $name;
    if (method_exists($this, $getter)) {
        // read property, e.g. getName()
        return $this->$getter();
    }

    // behavior property
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canGetProperty($name)) {
            return $behavior->$name;
        }
    }

    if (method_exists($this, "set" . $name)) {
        throw new InvalidCallException("Getting write-only property: " . get_class($this) . "::" . $name);
    }

    throw new UnknownPropertyException("Getting unknown property: " . get_class($this) . "::" . $name);
}

又似曾相识?是的,跟yiiaseBaseObject里面属性的实现类似,有兴趣的小伙伴可以看一下浅析Yii2.0的属性Property。然后直接看注释behavior property部分,又去调用了ensureBehaviors(),先不管,接着又去遍历_behaviors这个属性,根据上面$myClass的数据结构得知,此时foreach里面的$behavior就是行为类commoncomponentsBehaviorTest实例,先通过canGetProperty判断_val是否可读或者存在,大家可以去看yiiaseBaseObject里面该方法的实现。我们这里返回的是true,然后就直接通过commoncomponentsBehaviorTest的实例$behavior返回_val的值。

根据上面获取行为类里面属性的流程我们注意到:

因为是通过实例化行为类去调用的属性,所以属性是protected或者是private是获取不到的。

如果Component绑定了多个行为,并且多个行为中有同名的属性,那么该Component获取的是第一个行为类里面的该属性。

那么行为类里面的方法是怎么被调用的呢?属性的调用是通过__get()来实现的,很容易联想到方法的调用是通过__call()来实现的,我们查看一下yiiaseBaseObject源码,果然里面实现__call()这个魔术方法,下面是源码,然后对照上面$myClass的数据结构一看就明白了。需要注意的是,跟上面属性的调用一样,方法也必须是public的,protected 、private方法是调用不了的。

public function __call($name, $params)
{
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $object) {
        if ($object->hasMethod($name)) {
            return call_user_func_array([$object, $name], $params);
        }
    }
    throw new UnknownMethodException("Calling unknown method: " . get_class($this) . "::$name()");
}

注意到__call()里面又有一个老朋友ensureBehaviors()这个函数似乎无处不在?是的,查看一下yiiaseComponent里面的源码我们可以发现所有的公有方法都调用了这个函数,那么这个函数到底是干嘛的呢,其实demo里面绑定行为的方式可以称为主动绑定,就是我们主动调用函数attachBehavior()去绑定行为的,对应的就是被动绑定了,实现方式就是在待绑定行为的类里面重载behaviors()这个函数就可以实现绑定了,相当于一个行为的配置项。俩种绑定方式看个人喜好了,如果一个类需要绑定的行为很明确,推荐使用配置项的方法去绑定,也就是被动绑定。下面是将demo里面的绑定方式改成被动绑定。

 new BehaviorTest()
        ];
    }
}

此时可以将demo中 $myClass->attachBehavior("test", $myBehavior); 这句代码去掉,$myClass也是同样可以调用类 BehaviorTest 里面的属性和方法
小结

这俩天查看了Yii2.0事件、行为的实现方式,觉得有很多相似的地方,都是通过yiiaseComponent来实现的,通过打印的数据结构也可以看到,Component主要就是围绕_events _eventWildcards _behaviors这三个属性展开的,其中第二个属性是 事件的通配符模式,也可以归属到 事件中,那么这样Component的主要功能就是就是实现了 事件和行为。并且实现原理上也是相似的,都是往Component里面绑定事件和行为的handle,然后触发事件或者行为的时候,再去回调相应的handle。不过在解除的时候虽然都是删掉相应的handle,但是解除行为还需要解除在绑定行为的时候绑定的事件,这点不太一样。

以上总结参考了深入理解Yii2.0,其实以前就看过,但是也只是局限于看过,没有自己跑demo调试、查看源代码,然后就误以为自己明白了,其实过俩天什么都不记得了。所以现在通过写博客来加深自己的理解,由于水平有限,欢迎小伙伴交流和指正。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31590.html

相关文章

  • 浅析Yii2.0属性Property

    摘要:概念理解第一次看深入理解的时候,我也是懵逼的,属性不就是类的属性吗,有什么好说的。属性的实现步骤继承自。声明一个用于保存该属性的私有成员变量。如果只提供了,那么该属性为只读属性,只提供了,则为只写。 概念理解:第一次看深入理解Yii2.0的时候,我也是懵逼的,属性不就是类的属性吗,有什么好说的。后来才知道Yii框架对成员变量和属性做了区分,那类的成员变量和属性到底是什么关系又有什么区别...

    arashicage 评论0 收藏0
  • 浅析Yii2.0事件Event

    摘要:全局级别利用实例在整个应用的生命周期中全局可访问的特性,来实现这个全局事件的。类级别通过维护类的属性数组,触发事件时通过类名和事件名称取到当前类以及父类的数据,再通过函数触发。 概念理解:在某一个事件(trigger)发生的时候,触发预先设定(on)的代码,这是代码解耦的一种方式。 事件按照级别分为三类 1. 实例级别 绑定事件、触发事件的类继承的是Component,只在当前示例中运...

    李义 评论0 收藏0
  • Yii源码解读-行为

    摘要:行为所要响应的事件重载方法,表示这个行为将对类何种事件进行何种反馈。行为用的最多的,也是对于各种事件的响应。当出现命名冲突时,行为会自行排除冲突,自动使用先绑定的行为。目前还没有能支持行为。 Yii基础 行为(Behavior) 行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。 通过将行为绑定到一个类,可以使类具有行为本身所定义的属性和方法,就好像类本来就有这些...

    IntMain 评论0 收藏0
  • 游戏AI—行为树研究及实现

    摘要:另外,当并行器满足条件提前退出时,所有正在执行的子行为也应该立即被终止,我们在函数中调用每个子节点的终止方法监视器监视器是并行器的应用之一,通过在行为运行过程中不断检查是否满足某条件,如果不满足则立刻退出。将条件放在并行器的尾部即可。 从上古卷轴中形形色色的人物,到NBA2K中挥洒汗水的球员,从使命召唤中诡计多端的敌人,到刺客信条中栩栩如生的人群。游戏AI几乎存在于游戏中的每个角落,默...

    Harriet666 评论0 收藏0
  • 使用 CSS overscroll-behavior 控制滚动行为:自定义下拉刷新和溢出效果

    摘要:否则会出现两个下拉刷新之前之后禁用炫光和回弹效果将属性制定为,可以禁用默认的滚动边界效果。完整的地址源码最终效果阅读原文讨论地址使用控制滚动行为自定义下拉刷新和溢出效果如果你想参与讨论,请点击这里 dev-reading/fe 是一个阅读、导读、速读的 repo,不要依赖于 dev-reading/fe 学习知识。本 repo 只是一个快速了解文章内容的工具,并不提供全文解读和翻译。你...

    jhhfft 评论0 收藏0

发表评论

0条评论

legendmohe

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<