vue2.x源码解析系列二: Vue组件初始化过程概要

这里分析的是当前(2018/07/25)最新版 V2.5.16 的源码,如果你想一遍看一遍参阅源码,请务必记得切换到此版本,不然可能存在微小的差异。

大家都知道,我们的应用是一个由Vue组件构成的一棵树,其中每一个节点都是一个 Vue 组件。我们的每一个Vue组件是如何被创建出来的,创建的过程经历了哪些步骤呢?把这些都搞清楚,那么我们对Vue的整个原理将会有很深入的理解。

从入口函数开始,有比较复杂的引用关系,为了方便大家理解,我画了一张图可以直观地看出他们之间的关系:

创建Vue实例的两步

我们创建一个Vue实例,只需要两行代码:

而这两步分别经历了一个比较复杂的构建过程:

  1. 创建类:创建一个 Vue 构造函数,以及他的一系列原型方法和类方法
  2. 创建实例:创建一个 Vue 实例,初始化他的数据,事件,模板等

下面我们分别解析这两个阶段,其中每个阶段 又分为好多个 步骤

第一阶段:创建Vue类

第一阶段是要创建一个Vue类,因为我们这里用的是原型而不是ES6中的class声明,所以拆成了三步来实现:

  1. 创建一个构造函数 Vue
  2. Vue.prototype 上创建一系列实例属性方法,比如 this.$data
  3. Vue 上创建一些全局方法,比如 Vue.use 可以注册插件

我们导入 Vue 构造函数 import Vue from ‘vue’ 的时候(new Vue(options) 之前),会生成一个Vue的构造函数,这个构造函数本身很简单,但是他上面会添加一系列的实例方法和一些全局方法,让我们跟着代码来依次看看如何一步步构造一个 Vue 类的,我们要明白每一步大致是做什么的,但是这里先不深究,因为我们会在接下来几章具体讲解每一步都做了什么,这里我们先有一个大致的概念即可。

我们看代码先从入口开始,这是我们在浏览器环境最常用的一个入口,也就是我们 import Vue 的时候直接导入的,它很简单,直接返回了 从 platforms/web/runtime/index/js 中得到的 Vue 构造函数,具体代码如下:

platforms/web/entry-runtime.js

可以看到,这里不是 Vue 构造函数的定义地方,而是返回了从下面一步得到的Vue构造函数,但是做了一些平台相关的操作,比如内置 directives 注册等。这里就会有人问了,为什么不直接定义一个构造函数,而是这样不停的传递呢?因为 vue 有不同的运行环境,而每一个环境又有带不带 compiler 等不同版本,所以环境的不同以及版本的不同都会导致 Vue 类会有一些差异,那么这里会通过不同的步骤来处理这些差异,而所有的环境版本都要用到的核心代码是相同的,因此这些相同的代码就统一到 core/中了。

完整代码和我加的注释如下:

platforms/web/runtime/index.js

上面的代码终于把平台和配置相关的逻辑都处理完了,我们可以进入到了 core 目录,这里是Vue组件的核心代码,我们首先进入 core/index文件,发现 Vue 构造函数也不是在这里定义的。不过这里有一点值得注意的就是,这里调用了一个 initGlobalAPI 函数,这个函数是添加一些全局属性方法到 Vue 上,也就是类方法,而不是实例方法。具体他是做什么的我们后面再讲

core/index.js

core/instance/index.js 这里才是真正的创建了 Vue 构造函数的地方,虽然代码也很简单,就是创建了一个构造函数,然后通过mixin把一堆实例方法添加上去。

core/instance/index.js 完整代码如下:

下面我们分成两段来讲解这些代码分别干了什么。

这里才是真正的Vue构造函数,注意其实很简单,忽略在开发模式下的警告外,只执行了一行代码 this._init(options)。可想而知,Vue初始化必定有很多工作要做,比如数据的响应化、事件的绑定等,在第二阶段我们会详细讲解这个函数到底做了什么。这里我们暂且跳过它。

上面这五个函数其实都是在Vue.prototype上添加了一些属性方法,让我们先找一个看看具体的代码,比如initMixin 就是添加 _init 函数,没错正是我们构造函数中调用的那个 this._init(options) 哦,它里面主要是调用其他的几个初始化方法,因为比较简单,我们直接看代码:

core/instance/init.js

另外的几个同样都是在 Vue.prototype 上添加了一些方法,这里暂时先不一个个贴代码,总结一下如下:

  1. core/instance/state.js,主要是添加了 $data,$props,$watch,$set,$delete 几个属性和方法
  2. core/instance/events.js,主要是添加了 $on,$off,$once,$emit 三个方法
  3. core/instance/lifecycle.js,主要添加了 _update, $forceUpdate, $destroy 三个方法
  4. core/instance/renderMixin.js,主要添加了 $nextTick_render 两个方法以及一大堆renderHelpers

还记得我们跳过的在core/index.js中 添加 globalAPI的代码吗,前面的代码都是在 Vue.prototype 上添加实例属性,让我们回到 core/index 文件,这一步需要在 Vue 上添加一些全局属性方法。前面讲到过,是通过 initGlobalAPI 来添加的,那么我们直接看看这个函数的样子:

至此,我们就构建出了一个 Vue 类,这个类上的方法都已经添加完毕。这里再次强调一遍,这个阶段只是添加方法而不是执行他们,具体执行他们是要到第二阶段的。总结一下,我们创建的Vue类都包含了哪些内容:

上述就是我们的 Vue 类的全部了,有一些特别细小的点暂时没有列出来,如果你在后面看代码的时候,发现有哪个函数不知道在哪定义的,可以参考这里。那么让我们进入第二个阶段:创建实例阶段

第二阶段:创建 Vue 实例

我们通过 new Vue(options) 来创建一个实例,实例的创建,肯定是从构造函数开始的,然后会进行一系列的初始化操作,我们依次看一下创建过程都进行了什么初始化操作:

core/instance/index.js, 构造函数本身只进行了一个操作,就是调用 this._init(options) 进行初始化,这个在前面也提到过,这里就不贴代码了。

core/instance/init.js 中会进行真正的初始化操作,让我们详细看一下这个函数具体都做了些什么。

先看看它的完整代码:

我们来一段一段看看上面的代码分别作了什么。

这段代码首先生成了一个全局唯一的id。然后如果是非生产环境并且开启了 performance,那么会调用 mark 进行performance标记,这段代码就是开发模式下收集性能数据的,因为和Vue本身的运行原理无关,我们先跳过。

上面这段代码,暂时先不用管_isComponent,暂时只需要知道我们自己开发的时候使用的组件,都不是 _isComponent,所以我们会进入到 else语句中。这里主要是进行了 options的合并,最终生成了一个 $options 属性。下一章我们会详细讲解 options 合并的时候都做了什么,这里我们只需要暂时知道,他是把构造函数上的options和我们创建组件时传入的配置 options 进行了一个合并就可以了。正是由于合并了这个全局的 options 所以我们在可以直接在组件中使用全局的 directives

这段代码可能看起来比较奇怪,这个 renderProxy 是干嘛的呢,其实就是定义了在 render 函数渲染模板的时候,访问属性的时候的一个代理,可以看到生产环境下就是自己。

开发环境下作了一个什么操作呢?暂时不用关心,反正知道渲染模板的时候上下文就是 vm 也就是 this 就行了。如果有兴趣可以看看非生产环境,作了一些友好的报错提醒等。

这里只需要记住,在生产环境下,模板渲染的上下文就是 vm就行了。

这一段代码承担了组件初始化的大部分工作。我直接把每一步的作用写在注释里面了。 把这几个函数都弄懂,那么我们也就差不多弄懂了Vue的整个工作原理,而我们接下来的几篇文章,其实都是从这几个函数中的某一个开始的。

开始mount,注意这里如果是我们的options中指定了 el 才会在这里进行 $mount,而一般情况下,我们是不设置 el 而是通过直接调用 $mount("#app") 来触发的。比如一般我们都是这样的:

以上就是Vue实例的初始化过程。因为在 create 阶段和 $mount 阶段都很复杂,所以后面会分几个章节来分别详细讲解。下一篇,让我们从最神秘的数据响应化说起。

1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部