yii2-從behaviors()來研究元件繫結行為的原理

阿北哥ya發表於2017-08-21

不知不覺已經發布了7篇關於yii2行為的文章。傳送門,今天再分享一篇到掘金專欄。


為何使用 yii\base\Component::behaviors() 就能繫結行為,發生了什麼?

我們先來窺視一下類 Component 內部和繫結行為相關的函式。

  • yii\base\Component::behaviors()
  • yii\base\Component::ensureBehaviors()
  • yii\base\Component::attachBehaviorInternal()
  • yii\base\Behavior::attach()

behaviors()

behaviors() 函式上一篇已經講了,主要用來繫結行為的,裡面接收各種要繫結的行為,它返回了一個陣列,雖然我們現在知道配置這個函式能起到什麼效果,但是還是要研究下,我們先在yii2的目錄下搜尋下都哪些函式用了此函式。

只有一句?是的,通過搜尋我們發現只有一個函式呼叫了它 --- ensureBehaviors()。那就從它開始吧。

ensureBehaviors()

在研究它之前先看看程式碼

// 
/**
 * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component.
 */
public function ensureBehaviors()
{
    if ($this->_behaviors === null) {
        $this->_behaviors = [];
        foreach ($this->behaviors() as $name => $behavior) {
            $this->attachBehaviorInternal($name, $behavior);
        }
    }
}複製程式碼

邏輯很簡單,component元件類用一個屬性 _behaviors 來存放它擁有的所有行為物件,如果判斷為空,則呼叫$this->behaviors()函式獲取一下,對每個行為執行 attachBehaviorInternal()函式。

attachBehaviorInternal()

看函式名 attachBehaviorInternal() 是繫結行為的意思,那就看一看。

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;
}複製程式碼

在第一個if分支內判斷 $behavior 是否為 行為類Behavior的一個物件,如果不是則$behavior肯定是一些配置,那根據這些配置得到相關行為的物件。

總之 $behavior 已經是一個行為物件了,我們先看函式體最後一行,可以知道此函式返回了這個物件。

接下來我們來看第二個if分支。

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;
}複製程式碼

首先說對於 is_int($name) 的判斷,還記得我們在繫結行為的時候麼(傳送門),在 behaviors() 返回的陣列中,我們可以不為某個行為起名字,那叫做匿名指定,那自然這個key會是一個遞增的數字,所以 is_int($name) 在判斷是否為匿名行為。

如果是匿名行為,首先 $behavior->attach($this),然後放到 _behaviors 陣列中。

如果不是匿名行為,先看看 _behaviors 陣列中是否存在,如果存在則先 detach()後 $behavior->attach($this),然後放到 _behaviors 陣列中。

這樣一圈下來,_behaviors 陣列中存放一群行為物件,有些是匿名的,有些是有名字的。對吧。

那麼現在我們已經知道 attachBehaviorInternal函式的第一個功能 --- 填充 _behaviors 陣列,反過來回顧 ensureBehaviors的作用,這個ensureBehaviors的一個功能就是確保 _behaviors 陣列中有該元件應該有的所有行為物件。

為什麼是第一個那???因為在 attachBehaviorInternal中我們發現除了填充陣列外,還有一個叫做 $behavior->attach($this);的函式,它也將成為 attachBehaviorInternal / ensureBehaviors 功能之一。

那麼 attach() 函式做了什麼那?

attach()

先看一看它的程式碼,它在 vendor/yiisoft/yii2/base/Behavior.php 中,被行為物件呼叫。

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

分析一下,在元件處理自己行為的時候,將$this傳遞給了行為物件的方法 $behavior->attach($this),而在行為的 attach 方法中 $this->owner = $owner 一下,這意為著什麼?

元件的每個行為物件都有一個屬性owner存放了使用他們的元件物件,到此刻元件有 _behaviors 陣列存放自己的所有行為物件,而行為有owner屬性存放使用了自己的元件物件,它們建立了雙向聯絡。

而關於在attach中的foreach迴圈體主要是處理事件的,我們會在行為和事件一篇說明。

此刻,我們再來歸納一下 ensureBehaviors 的功能,也就是繫結方法背後都觸發了哪些動作

  1. 我們在元件的子類(比如AR、控制器等)中使用behaviors()來繫結一些行為。
  2. 然後有一個叫做 ensureBehaviors 的函式確保了此元件物件和繫結的行為物件可以彼此擁有。

但是

我們都知道,繫結行為後,元件物件就可以像使用自身屬性和方法一樣操作,這似乎和 ensureBehaviors 沒有啥關係,下篇將為你解析當我們直接呼叫行為屬性的時候,發生了什麼?以及在這其中 ensureBehaviors 起到了什麼作用?

相關文章