Yii2源码分析 之 组件加载过程

08-23 10:08 字数 6640 阅读 129 已编辑

平时可能我们对于这样的写法已经司空见惯了 Yii::$app->db 或者 Yii::$app->redis,那么到底 dbredis组件是怎么加载的呢,带着这样的疑问,再查询了源码和资料后发现了,这些组件并不是在系统运行的时候就直接创建的,而是在使用到的时候才去创建,本文记录下源码学习过程。

首先自然从入口文件index.php开始

require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.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'
);

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

可以看到所有的配置被合并之后交给了yii\web\Application,我们配置的db组件和redis组件自然也被带过去了,所以自然先去这个类的构造函数,但是跳转过去之后发现这个类没有构造函数,所以自然去找它的父类yii\base\Application的构造函数。

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

    $this->state = self::STATE_BEGIN;

    $this->preInit($config);

    $this->registerErrorHandler($config);

    Component::__construct($config);
}

分析这段代码:

Yii::$app = $this;
首先把当前实例yii\web\Application赋值给了Yii::$app

static::setInstance($this);
yii\base\Module的方法,主要是把当前类和实例的关系保存到loadedModules数组里。

$this->preInit($config);
主要是做一些初始化操作,比如设置时区和runtime路径,并且会合并系统的核心组件到$config中。

$this->registerErrorHandler($config);
则主要是接管默认的异常和错误处理。

到目前位置好像还是没看到我们配置文件中的组件在哪里加载并实例化的,接着分析构造函数的最后一句代码。

Component::__construct($config);
这段代码调用的是yii\base\Component的构造函数,但是这个类没有构造函数,实际调用的是其父类yii\base\BaseObject的构造函数。

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

首先看第一句Yii::configure($this, $config);
实际调用的是yii\baseYiiconfigure方法

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

    return $object;
}

这段代码的意思就是缓存遍历配置项数组 $config 比如 idnamecomponents 然后赋值给yii\web\Application实例,我们的dbredis组件都在 components数组中,所以也就是调用了 Yii::$app->components = $value,但是查找yii\web\Application类,并没有发现 components属性啊。

别着急,我们先把yii\web\Application的继承链画出来。

yii\web\Application继承图

根据常识、在PHP中给类不存在的属性赋值,会调用__set魔术方法,所以我们在原型链上找到yii\web\Application最近的父类的__set()方法,最后发现最近的__set()方法在 yii\base\Component中。

public function __set($name, $value)
{
    $setter = 'set' . $name;
    if (method_exists($this, $setter)) {
        // set property
        $this->$setter($value);

        return;
    } elseif (strncmp($name, 'on ', 3) === 0) {
        // on event: attach event handler
        $this->on(trim(substr($name, 3)), $value);

        return;
    } elseif (strncmp($name, 'as ', 3) === 0) {
        // as behavior: attach behavior
        $name = trim(substr($name, 3));
        $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));

        return;
    }

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

    if (method_exists($this, 'get' . $name)) {
        throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
    }

    throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}

我们只需要看第一个if语句中的代码,如果发现有setcomponents方法,则去调用setcomponents它,那么yii\web\Application的父类中有没有setcomponents方法呢?有,在yii\di\ServiceLocator类中。

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

这个代码做的事情也很简单,就是遍历components中的各个配置数组,调用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;
            $this->_definitions[$id]['class'] = $definition['__class'];
            unset($this->_definitions[$id]['__class']);
        } elseif (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));
    }
}

分析上面的代码可以看出,其实这个方法就是清空_components属性,然后把配置文件中componetns的各项组件配置保存到_definitions属性中,然后就完了???

通过上面一通分析,我们可以看出,在应用运行的时候,Yii并没有去实例化组件,仅仅是把他们的保存了起来,那么当我们访问Yii::$app->db的属性时会发生什么呢?根据PHP常识,访问一个类中不存在的属性时,会调用__get魔术方法,所以我们只需要找到yii\web\Application或者离他最近的父类中的__get方法就行了,最后在yii\di\ServiceLocator中找到了__get方法。

public function __get($name)
{
    if ($this->has($name)) {
        return $this->get($name);
    }

    return parent::__get($name);
}

首先是has方法,判断db是否存在。

public function has($id, $checkInstance = false)
{
    return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}

$checkInstance默认值为false,也就是判断_definitions数组中,是否有db元素,根据上面的分析,我们已经把所有的components都保存在了_definitions数组中,所以这个判断自然返回true。

所以继续直接返回了$this->get(db);,我们来看下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];
        if (is_object($definition) && !$definition instanceof Closure) {
            return $this->_components[$id] = $definition;
        }

        return $this->_components[$id] = Yii::createObject($definition);
    } elseif ($throwException) {
        throw new InvalidConfigException("Unknown component ID: $id");
    }

    return null;
}

分析下代码我们得知,最终执行了这一句 return $this->_components[$id] = Yii::createObject($definition);,也就是当我们调用Yii::$app->db时,最终是调用Yii::createObject并且把返回结果保存在了_components属性中,这样下次再调用Yii::$app->db时,就直接返回了_components中的db元素。 至于Yii::createObject做了什么则不在本次分析之中,我们下次再说。

最后总结一下: 当我们首次运行应用时,我们所有在配置文件中定义好的组件components只是保存在了yii\di\ServiceLocator_definitions属性中,并没有创建这些组件,而是当我们真正第一次调用组件时才去创建组件,并且保存在_components属性中,这样下次再调用相同组件的时候,就不用在此创建,而是直接返回组件的实例。

这种组件的懒加载还是值得我们学习的,用到的时候再去创建实例,创建好之后保存起来供下次使用,这对系统的性能是有好处的。因为这样节省了创建实例的时间和内存。

1人点赞>
关注 收藏 改进 举报
0 条评论
排序方式 时间 投票
快来抢占一楼吧
请登录后发表评论
站长 @ 十七度
文章
347
粉丝
23
喜欢
170
收藏
22
排名 : 1
访问 : 75.51万
私信