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中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值