理解Yii2類的延遲加載

目錄(?)[+]

Yii的類自動加載機制

在Yii中,所有類、接口、Traits都可以使用類的自動加載機制實現在調用前自動加載。Yii藉助了PHP的類自動加載機制高效實現了類的定位、導入,這一機制兼容 PSR-4 的標準。在Yii中,類僅在調用時纔會被加載,特別是核心類,其定位非常快,這也是Yii高效高性能的一個重要體現。

自動加載機制的實現

先看一自動加載的簡單例子

<?php

//Yii框架也根據這種機制實現了類的延遲加載,做得更加地道(使用spl_autoload_register()自動完成類的加載)。
//======下方的1 2 3 4爲執行順序
//4.當PHP發現實例化時不認識Class1,它會把Class1的名字傳遞給my_loader函數的$class參數當中,那麼$class變量就代表了Class1。(同理,當PHP實例化Class2時也如此)
function my_loader($class){
    //3.但是這裏不僅加載了Class1,還加載了Class2,導致類還是會被多餘加載。怎樣解決這個問題?
    /*require('class\Class1.php');
    require('class\Class2.php');*/
    //5.所以可以通過$class對require代碼進行優化:把Class1改爲$class變量,因爲\具有轉義的意思,所以需要2個\\來代表1個\。
    require('class\\'.$class.'.php');
}
//2.這是由於PHP準備報錯時,這個函數告訴PHP先不報錯,先去運行一下my_loader。然後PHP真的去運行my_loader函數,把裏面的Class1類文件加載進來了。也就不會報錯了。
spl_autoload_register('my_loader');
$is_girl = !empty($_GET['sex']) && htmlspecialchars($_GET['sex']) == 2 ? true : false;//http://127.0.0.1/test/test.php?sex=2
if ($is_girl) {
    echo 'this is a girl!';
    $Class1 = new Class1; //1.當PHP運行到這一行,不認識Class1並且在這裏也沒有加載類文件,這時候PHP八成會報錯,但實際上並沒有報錯。
} else {
    echo 'not a girl!';
    $Class2 = new Class2;
}


//測試類
class Class1
{
    public function __construct() {
        echo '<br/>';
        echo 'I am class1';
    }
}
class Class2
{
    public function __construct() {
        echo '<br/>';
        echo 'I am class1';
    }    
}

理解了上面的例子後再看下發的Yii實現原理就更好理解了

Yii的類自動加載,依賴於PHP的 spl_autoload_register() , 註冊一個自己的自動加載函數(autoloader),並插入到自動加載函數棧的最前面,確保Yii的autoloader會被最先調用。

類自動加載的這個機制的引入要從入口文件 index.php 開始說起:

<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');

// 這個是第三方的autoloader
require(__DIR__ . '/../../vendor/autoload.php');

// 這個是Yii的Autoloader,放在最後面,確保其插入的autoloader會放在最前面
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
// 後面不應再有autoloader了

require(__DIR__ . '/../../common/config/aliases.php');

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-local.php'),
    require(__DIR__ . '/../config/main.php'),
    require(__DIR__ . '/../config/main-local.php')
);

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

這個文件主要看點在於第三方autoloader與Yii 實現的autoloader的順序。不管第三方的代碼是如何使用 spl_autoload_register() 來註冊自己的autoloader的,只要Yii 的代碼在最後面,就可以確保其可以將自己的autoloader插入到整個autoloder 棧的最前面,從而在需要時最先被調用。(解釋:棧是先進後出的,所以Yii將自己的autoloader放在後面)

類的映射表機制

類的映射(class map)表機制:Yii框架也提供了類的映射表機制去進一步地更快加載類(常用的類,不建議不常用的類也放在$classMap中,會讓在這個數組裏查找相應的類的速率降低同時會佔用更大的內存) 
//使用類的映射表去加載Order類,使用\Yii全局類裏的$classMap['參數key:加載的類的全名','']數組,因爲是要根據它的名字去加載它的絕對路徑,所以這個數組的值就是Order類所在類文件的絕對路徑。

\Yii::$classMap['app\models\Order'] = 'C:\wamp\www\mooc\yii\basic\modelsx\Order.php';
//實例化Order活動記錄
$order = new Order;

接下來,看看Yii是如何調用 spl_autoload_register() 註冊autoloader的, 這要看 Yii.php 裏發生了些什麼:

<?php
require(__DIR__ . '/BaseYii.php');
class Yii extends \yii\BaseYii
{
}

// 重點看這個 spl_autoload_register
spl_autoload_register(['Yii', 'autoload'], true, true);

// 下面的語句讀取了一個映射表
Yii::$classMap = include(__DIR__ . '/classes.php');

Yii::$container = new yii\di\Container;

這段代碼,調用了 spl_autoload_register(['Yii', 'autoload', true, true]) ,將 Yii::autoload() 作爲autoloader插入到棧的最前面了。並將 classes.php 讀取到 Yii::$classMap 中,保存了一個映射表。

在上面的代碼中,Yii類是裏面沒有任何代碼,並未對 BaseYii::autoload() 進行重載,所以,這個 spl_autoload_register() 實際上將 BaseYii::autoload() 註冊爲autoloader。如果,你要實現自己的autoloader,可以在 Yii 類的代碼中,對 autoload() 進行重載。

在調用 spl_autoload_register() 進行autoloader註冊之後,Yii將 calsses.php 這個文件作爲一個映射表保存到 Yii::$classMap 當中。這個映射表,保存了一系列的類名與其所在PHP文件的映射關係,比如:

return [
  'yii\base\Action' => YII2_PATH . '/base/Action.php',
  'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',

  ... ...

  'yii\widgets\PjaxAsset' => YII2_PATH . '/widgets/PjaxAsset.php',
  'yii\widgets\Spaceless' => YII2_PATH . '/widgets/Spaceless.php',
];

這個映射表以類名爲鍵,以實際類文件爲值,Yii所有的核心類都已經寫入到這個 classes.php 文件中,所以,核心類的加載是最便捷,最快的。現在,來看看這個關鍵先生 BaseYii::autoload()

public static function autoload($className)
{
    if (isset(static::$classMap[$className])) {
        $classFile = static::$classMap[$className];
        if ($classFile[0] === '@') {
            $classFile = static::getAlias($classFile);
        }
    } elseif (strpos($className, '\\') !== false) {
        $classFile = static::getAlias('@' . str_replace('\\', '/',
            $className) . '.php', false);
        if ($classFile === false || !is_file($classFile)) {
            return;
        }
    } else {
        return;
    }

    include($classFile);

    if (YII_DEBUG && !class_exists($className, false) &&
        !interface_exists($className, false) && !trait_exists($className,
        false)) {
        throw new UnknownClassException(
        "Unable to find '$className' in file: $classFile. Namespace missing?");
    }
}

從這段代碼來看Yii類自動加載機制的運作原理:

檢查 $classMap[$className] 看看是否在映射表中已經有擬加載類的位置信息;

如果有,再看看這個位置信息是不是一個路徑別名,即是不是以 @ 打頭, 是的話,將路徑別名解析成實際路徑。 如果映射表中的位置信息並非一個路徑別名,那麼將這個路徑作爲類文件的所在位置。 類文件的完整路徑保存在 $classFile ;

如果 $classMap[$className] 沒有該類的信息, 那麼,看看這個類名中是否含有 \ , 如果沒有,說明這是一個不符合規範要求的類名,autoloader直接返回。 PHP會嘗試使用其他已經註冊的autoloader進行加載。 如果有 \ ,認爲這個類名符合規範,將其轉換成路徑形式。 即所有的 \ 用 / 替換,並加上 .php 的後綴。

將替換後的類名,加上 @ 前綴,作爲一個路徑別名,進行解析。 從別名的解析過程我們知道,如果根別名不存在,將會拋出異常。 所以,類的命名,必須以有效的根別名打頭:

// 有效的類名,因爲@yii是一個已經預定義好的別名
use yii\base\Application;

// 無效的類名,因爲沒有 @foo 或 @foo/bar 的根別名,要提前定義好
use foo\bar\SomeClass;

使用PHP的 include() 將類文件加載進來,實現類的加載。

從其運作原理看,最快找到類的方式是使用映射表。 其次,Yii中所有的類名,除了符合規範外,還需要提前註冊有效的根別名。 
運用自動加載機制

在入口腳本中,除了Yii自己的autoloader,還有一個第三方的autoloader:

require(__DIR__ . '/../../vendor/autoload.php');

這個其實是Composer提供的autoloader。Yii使用Composer來作爲包依賴管理器,因此,建議保留Composer的autoloader,儘管Yii的autoloader也能自動加載使用Composer安裝的第三方庫、擴展等,而且更爲高效。但考慮到畢竟是人家安裝的,人家還有一套自己專門的規則,從維護性、兼容性、擴展性來考慮,建議保留Composer的autoloader。

如果還有其他的autoloader,一定要在Yii的autoloader註冊之前完成註冊,以保證Yii的autoloader總是最先被調用。

如果你有自己的autoloader,也可以不安裝Yii的autoloaer,只是這樣未必能有Yii的高效,且還需要遵循一套類似的類命名和加載的規則。就個人的經驗而言,Yii的autoloader完全夠用,沒必要自己重複造輪子。

至於Composer如何自動加載類文件,這裏就不過多的佔用篇幅了。可以看看 Composer的文檔 。

組件的延時加載

組件的延遲加載:比如用戶給Yii框架的項目發送了一個請求,index.php入口腳本文件最先處理這個請求->再把請求交給應用主體app處理(在處理請求之前,把它自己給實例化出來,實例化過程當中會去加載組件<-組件components[包含:session/request/response…組件])->app加載完組件之後再把請求交給Controller處理(控制器在處理請求時可以使用app加載過來的組件)。

那麼所謂的組件延遲加載就是:看起來好像是app預先加載了components裏的組件,然後在Controller中直接拿過來用。實際上app並沒有真正的加載components裏的組件,而是在Controller裏真正使用到某一個組件(如session)時才加載進來,也就是說把這個組件的加載過程由app的初始化延遲到Controller真正的使用某一個組件時。 
實際上這個session組件在調用這行代碼之前是不存在的,只有調用了這行代碼,程序知道我們想使用session組件纔會去加載進來。 
具體加載流程:當訪問app應用主體裏的session屬性時,會觸及PHP當中的__get()方法,在__get()方法裏纔會真正的把session組件加載進來,加載完之後會把session組件返回出來(通過$session變量接收)。

$session = \Yii::$app->session;

參考文章:http://www.digpage.com/autoload.html

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