Yii-組件延遲加載機制

使用場景

$session = \YII::$app->session

YII框架中每個組件都是一個單例,通過服務定位模式,在組件使用之時才進行初始化。


準備知識

程序入口

index.php 中實例化 yii\web\Application

(new yii\web\Application($config))->run();

類繼承關係

瞭解下類繼承關係,方便後續尋找對應成員函數

class yii\web\Application extends yii\base\Application {}
abstract class \yii\base\Application extends yii\base\Module {}
class yii\base\Module extends yii\di\ServiceLocator {}
class yii\di\ServiceLocator extends yii\base\Component{}
class yii\base\Component extends yii\baseBaseObject{}

組件註冊流程

尋找構造函數

由於yii\web\Application類中無構造函數,所以只能一層一層找父類的構造,首先第一層就是\yii\base\Application中的構造函數。代碼如下:

public function __construct($config = [])
{
    Yii::$app = $this;
    static::setInstance($this);

    $this->state = self::STATE_BEGIN;

    //config 配置的初始化
    $this->preInit($config);

    //註冊錯誤句柄
    $this->registerErrorHandler($config);

    //組件初始化
    Component::__construct($config);
}

以上代碼中主要關注preInit以及Component::__construct


關注preInitComponent::__construct

preInit需要關注的相關代碼如下:

public function preInit(&$config)
{
    /* 此處省略其他預處理代碼 */
 
    // merge core components with custom components
    foreach ($this->coreComponents() as $id => $component) {
        if (!isset($config['components'][$id])) {
            $config['components'][$id] = $component;
        } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
            $config['components'][$id]['class'] = $component['class'];
        }
    }
}

首先在perInit中會獲取coreComponents中的數據,相關核心組件數據如下:

// 父類中的核心組件
abstract class \yii\base\Application extends yii\base\Module 
{
    public function coreComponents()
    {
        return [
            'log' => ['class' => 'yii\log\Dispatcher'],
            'view' => ['class' => 'yii\web\View'],
            'formatter' => ['class' => 'yii\i18n\Formatter'],
            'i18n' => ['class' => 'yii\i18n\I18N'],
            'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
            'urlManager' => ['class' => 'yii\web\UrlManager'],
            'assetManager' => ['class' => 'yii\web\AssetManager'],
            'security' => ['class' => 'yii\base\Security'],
        ];
    }	
}

//當前類中的核心組件
class yii\web\Application extends yii\base\Application 
{
    public function coreComponents()
    {
        return array_merge(parent::coreComponents(), [
            'request' => ['class' => 'yii\web\Request'],
            'response' => ['class' => 'yii\web\Response'],
            'session' => ['class' => 'yii\web\Session'],
            'user' => ['class' => 'yii\web\User'],
            'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
        ]);
    }
}

以上這些組件是Yii的核心組件,Yii會通過preInit首先寫入到$config['components']


Component::__construct中需要關注的相關代碼如下:

Component類中使用繼承來自yii\baseBaseObject中的構造函數

public function __construct($config = [])
{
    if (!empty($config)) {
   		Yii::configure($this, $config);
    }
    $this->init();
}

在構造函數中主要是調用了BaseYii中的靜態方法configure

public static function configure($object, $properties)
{
    foreach ($properties as $name => $value) {
    	$object->$name = $value;
    }

    return $object;
}

configureforeach循環執行到components時,$namecomponents$value爲所有組件的配置;此時給$object對象的components屬性進行賦值,但是yii\web\Application和其對應的父類中並沒有components成員變量,當賦值操作找不到components成員變量時,會去調用父類中__set方法嘗試尋找setComponents方法,該方法存在於yii\di\ServiceLocator中,具體代碼如下:

public function setComponents($components)
{
    foreach ($components as $id => $component) {
   		$this->set($id, $component);
    }
}

setComponents方法其實就是遍歷各個組件的配置,並調用yii\di\ServiceLocator中的set方法。


set方法具體代碼如下:

public function set($id, $definition)
{
	unset($this->_components[$id]);

	if ($definition === null) {
		unset($this->_definitions[$id]);
		return;
	}

    if (is_object($definition) || is_callable($definition, true)) {
        // an object, a class name, or a PHP callable
        $this->_definitions[$id] = $definition;
    } elseif (is_array($definition)) {
        // a configuration array
        if (isset($definition['class'])) {
        	$this->_definitions[$id] = $definition;
        } else {
        	throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
    	}
   	 } else {
    	throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
    }
}

set方法其實就是將組件配置存入$this->_definition,方便後續獲取。

整個組件註冊流程其實就是將所有核心組件配置信息分別註冊到yii\di\ServiceLocator類中的_definition私有成員中。


組件加載流程

當在代碼中調用相關核心組件時,由於不存在對應的成員變量,會一直找父類中的__get魔術方法,最終在yii\di\ServiceLocator中有對應的定義。

yii\di\ServiceLocator中的__get方法

具體的代碼參考如下:

class ServiceLocator extends Component
{    
    // 用於緩存服務、組件等的實例
    private $_components = [];

    // 用於保存服務和組件的定義,用於實例化對象
    private $_definitions = [];
    
    //將對象ID作爲ServiceLocator的屬性,可通過$serviceLocator->{ID}直接獲取
	public function __get($name)
    {
        if ($this->has($name)) {
            return $this->get($name);
        }

        return parent::__get($name);
    }

    //檢驗是否有對象$id
    public function has($id, $checkInstance = false)
    {
        return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
    }
}

整體步驟梳理如下:

  1. __get方法首先進入has方法,根據參數$checkInstance進行後續判斷
  2. $checkInstancefalse(默認值),該方法將返回一個值,指示_components中是否具有指定的組件定義。
  3. $checkInstancetrue,該方法將返回一個值,指示_definitions中是否有實例化指定的組件。
  4. 由於一開始$checkInstance值爲false,會直接返回isset($this->_definitions[$id])的結果,如果是Yii內部核心組件,由於提前自動註冊過,返回true,調用當前類的get方法。

yii\di\ServiceLocator 中的get 方法

public function get($id, $throwException = true)
{
     //已經實例化的,直接返回
    if (isset($this->_components[$id])) {
        return $this->_components[$id];
    }

	//已經註冊過該對象
    if (isset($this->_definitions[$id])) {
        $definition = $this->_definitions[$id];
         //有該對象的定義,且定義已經是一個對象,設置$_components並直接返回
        if (is_object($definition) && !$definition instanceof Closure) {
            return $this->_components[$id] = $definition;
        }

        // 有定義但未實例化,則交給DI Container去實例化,並且設置$_components
        return $this->_components[$id] = Yii::createObject($definition);
    } elseif ($throwException) {
        throw new InvalidConfigException("Unknown component ID: $id");
    }

    return null;
}

get方法中執行流程如下:

  • 對於_components中已經實例化記錄過的組件,直接返回對應組件實例
  • 對於_definitions中定義過但是未實現的組件,創建對應組件的實例,並以組件id作爲key存入_components中,下次再調用對應組件時,直接從_components中獲取對應實例返回。
  • _definitions中沒有對應組件註冊信息的組件,拋出異常

注:每個組件都是一個單例,只會被實例化一次


總結

註冊過程

Yii中對於核心組件會提前註冊到yii\di\ServiceLocator中的_definitions中,組件名稱作爲key,組件相關配置作爲value


加載過程

當調用核心組件時,首先判斷yii\di\ServiceLocator中的_components是否已經有實例化好的組件信息,若有直接返回,否則使用_definitions中對應組件的配置信息進行實例化返回,並將實例化的對象記錄到_components中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章