Vue2.x 源码解析:组件初始化过程概要

作者:lihongxun945

github.com/lihongxun945/myblog/issues/23


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



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


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



创建Vue实例的两步

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


import Vue from ‘vue'

new Vue(options)


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


创建类:创建一个 Vue 构造函数,以及他的一系列原型方法和类方法

创建实例:创建一个 Vue 实例,初始化他的数据,事件,模板等


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


第一阶段:创建Vue类

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


创建一个构造函数 Vue

在 Vue.prototype 上创建一系列实例属性方法,比如 this.$data 等

在 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


import Vue from './runtime/index'

export default Vue


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


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


platforms/web/runtime/index.js


import Vue from 'core/index'

import config from 'core/config'

// 省略


import platformDirectives from './directives/index'

import platformComponents from './components/index'


//这里都是web平台相关的一些配置

// install platform specific utils

Vue.config.mustUseProp = mustUseProp

// 省略


// 注册指令和组件,这里的 directives 和 components 也是web平台上的,是内置的指令和组件,其实很少

// install platform runtime directives & components

extend(Vue.options.directives, platformDirectives) // 内置的directives只有两个,`v-show` 和 `v-model`

extend(Vue.options.components, platformComponents) // 内置的组件也很少,只有`keepAlive`, `transition`和 `transitionGroup`


// 如果不是浏览器,就不进行 `patch` 操作了

// install platform patch function

Vue.prototype.__patch__ = inBrowser ? patch : noop


// 如果有 `el` 且在浏览器中,则进行 `mount` 操作

// public mount method

Vue.prototype.$mount = function (

  el?: string | Element,

  hydrating?: boolean

): Component {

  el = el && inBrowser ? query(el) : undefined

  return mountComponent(this, el, hydrating)

}


// 省略devtool相关代码


export default Vue


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


core/index.js


import Vue from './instance/index'

import { initGlobalAPI } from './global-api/index'


initGlobalAPI(Vue) // 这个函数添加了一些类方法属性


// 省略一些ssr相关的内容

// 省略


Vue.version = '__VERSION__'


export default Vue


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


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


//  省略import语句

function Vue (options) {

  if (process.env.NODE_ENV !== 'production' &&

    !(this instanceof Vue)

  ) {

    warn('Vue is a constructor and should be called with the `new` keyword')

  }

  this._init(options)

}


initMixin(Vue)

stateMixin(Vue)

eventsMixin(Vue)

lifecycleMixin(Vue)

renderMixin(Vue)


export default Vue


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


function Vue (options) {

  if (process.env.NODE_ENV !== 'production' &&

    !(this instanceof Vue)

  ) {

    warn('Vue is a constructor and should be called with the `new` keyword')

  }

  this._init(options) // 构造函数有用的只有这一行代码,是不是很简单,至于这一行代码具体做了什么,在第二阶段我们详细讲解。

}


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


initMixin(Vue)

stateMixin(Vue)

eventsMixin(Vue)

lifecycleMixin(Vue)

renderMixin(Vue)


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


core/instance/init.js


export function initMixin (Vue: Class<Component>) {

  // 就是这里,添加了一个方法

  Vue.prototype._init = function (options?: Object) {

    // 省略,这部分我们会在第二阶段讲解

  }

}


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


core/instance/state.js,主要是添加了 $data,$props,$watch,$set,$delete 几个属性和方法

core/instance/events.js,主要是添加了 $on,$off,$once,$emit 三个方法

core/instance/lifecycle.js,主要添加了 _update, $forceUpdate, $destroy 三个方法

core/instance/renderMixin.js,主要添加了 $nextTick 和 _render 两个方法以及一大堆renderHelpers


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


export function initGlobalAPI (Vue: GlobalAPI) {

  // config

  const configDef = {}

  configDef.get = () => config

  // 省略


  // 这里添加了一个`Vue.config` 对象,至于在哪里会用到,后面会讲

  Object.defineProperty(Vue, 'config', configDef)


  // exposed util methods.

  // NOTE: these are not considered part of the public API - avoid relying on

  // them unless you are aware of the risk.

  Vue.util = {

    warn,

    extend,

    mergeOptions,

    defineReactive

  }


  //一般我们用实例方法而不是这三个类方法

  Vue.set = set

  Vue.delete = del

  Vue.nextTick = nextTick


  // 注意这里,循环出来的结果其实是三个 `components`,`directives`, `filters`,这里先创建了空对象作为容器,后面如果有对应的插件就会放进来。

  Vue.options = Object.create(null)

  ASSET_TYPES.forEach(type => {

    Vue.options[type + 's'] = Object.create(null)

  })


  // this is used to identify the "base" constructor to extend all plain-object

  // components with in Weex's multi-instance scenarios.

  Vue.options._base = Vue


  // 内置组件只有一个,就是 `keepAlive`

  extend(Vue.options.components, builtInComponents)


  initUse(Vue) // 添加了 Vue.use 方法,可以注册插件

  initMixin(Vue) //添加了Vue.mixin 方法

  initExtend(Vue) // 添加了 Vue.extend 方法


  // 这一步是注册了 `Vue.component` ,`Vue.directive` 和 `Vue.filter` 三个方法,上面不是有 `Vue.options.components` 等空对象吗,这三个方法的作用就是把注册的组件放入对应的容器中。

  initAssetRegisters(Vue)

}


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


//构造函数

function Vue () {

  this._init()

}


//全局config对象,我们几乎不会用到

Vue.config = {

  keyCodes,

  _lifecycleHooks: ['beforeCreate', 'created', ...]

}


// 默认的options配置,我们每个组件都会继承这个配置。

Vue.options = {

  beforeCreate, // 比如 vue-router 就会注册这个回调,因此会每一个组件继承

  components, // 前面提到了,默认组件有三个 `KeepAlive`,`transition`, `transitionGroup`,这里注册的组件就是全局组件,因为任何一个组件中不用声明就能用了。所以全局组件的原理就是这么简单

  directives, // 默认只有 `v-show` 和 `v-model`

  filters // 不推荐使用了

}


//一些全局方法

Vue.use // 注册插件

Vue.component // 注册组件

Vue.directive // 注册指令

Vue.nextTick //下一个tick执行函数

Vue.set/delete // 数据的修改操作

Vue.mixin // 混入mixin用的


//Vue.prototype 上有几种不同作用的方法


//由initMixin 添加的 `_init` 方法,是Vue实例初始化的入口方法,会调用其他的功能初始话函数

Vue.prototype._init


// 由 initState 添加的三个用来进行数据操作的方法

Vue.prototype.$data

Vue.prototype.$props

Vue.prototype.$watch


// 由initEvents添加的事件方法

Vue.prototype.$on

Vue.prototype.$off

Vue.prototype.$one

Vue.prototype.$emit


// 由 lifecycle添加的生命周期相关的方法

Vue.prototype._update

Vue.prototype.$forceUpdate

Vue.prototype.$destroy


//在 platform 中添加的生命周期方法

Vue.prototype.$mount


// 由renderMixin添加的`$nextTick` 和 `_render` 以及一堆renderHelper

Vue.prototype.$nextTick

Vue.prototype._render

Vue.prototype._b

Vue.prototype._e

//...


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


第二阶段:创建 Vue 实例

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


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


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


先看看它的完整代码:


Vue.prototype._init = function (options?: Object) {

  const vm: Component = this

  // a uid

  vm._uid = uid++


  let startTag, endTag

  /* istanbul ignore if */

  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

    startTag = `vue-perf-start:${vm._uid}`

    endTag = `vue-perf-end:${vm._uid}`

    mark(startTag)

  }


  // a flag to avoid this being observed

  vm._isVue = true

  // merge options

  if (options && options._isComponent) {

    // optimize internal component instantiation

    // since dynamic options merging is pretty slow, and none of the

    // internal component options needs special treatment.

    initInternalComponent(vm, options)

  } else {

    vm.$options = mergeOptions(

      resolveConstructorOptions(vm.constructor),

      options || {},

      vm

    )

  }

  /* istanbul ignore else */

  if (process.env.NODE_ENV !== 'production') {

    initProxy(vm)

  } else {

    vm._renderProxy = vm

  }

  // expose real self

  vm._self = vm

  initLifecycle(vm)

  initEvents(vm)

  initRender(vm)

  callHook(vm, 'beforeCreate')

  initInjections(vm) // resolve injections before data/props

  initState(vm)

  initProvide(vm) // resolve provide after data/props

  callHook(vm, 'created')


  /* istanbul ignore if */

  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

    vm._name = formatComponentName(vm, false)

    mark(endTag)

    measure(`vue ${vm._name} init`, startTag, endTag)

  }


  if (vm.$options.el) {

    vm.$mount(vm.$options.el)

  }

}


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


const vm: Component = this // vm 就是this的一个别名而已

    // a uid

    vm._uid = uid++ // 唯一自增ID


    let startTag, endTag

    /* istanbul ignore if */

    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

      startTag = `vue-perf-start:${vm._uid}`

      endTag = `vue-perf-end:${vm._uid}`

      mark(startTag)

    }


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


 // a flag to avoid this being observed

    vm._isVue = true

    // merge options

    //

    // TODO

    if (options && options._isComponent) {

      // optimize internal component instantiation

      // since dynamic options merging is pretty slow, and none of the

      // internal component options needs special treatment.

      initInternalComponent(vm, options)

    } else {

      // mergeOptions 本身比较简单,就是做了一个合并操作

      vm.$options = mergeOptions(

        resolveConstructorOptions(vm.constructor),

        options || {},

        vm

      )

    }


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


 /* istanbul ignore else */

    if (process.env.NODE_ENV !== 'production') {

      initProxy(vm)

    } else {

      vm._renderProxy = vm

    }


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


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


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


 // expose real self

    vm._self = vm


    initLifecycle(vm) // 做了一些生命周期的初始化工作,初始化了很多变量,最主要是设置了父子组件的引用关系,也就是设置了 `$parent` 和 `$children`的值

    initEvents(vm) // 注册事件,注意这里注册的不是自己的,而是父组件的。因为很明显父组件的监听器才会注册到孩子身上。

    initRender(vm) // 做一些 render 的准备工作,比如处理父子继承关系等,并没有真的开始 render

    callHook(vm, 'beforeCreate') // 准备工作完成,接下来进入 `create` 阶段

    initInjections(vm) // resolve injections before data/props

    initState(vm) // `data`, `props`, `computed` 等都是在这里初始化的,常见的面试考点比如`Vue是如何实现数据响应化的` 答案就在这个函数中寻找

    initProvide(vm) // resolve provide after data/props

    callHook(vm, 'created') // 至此 `create` 阶段完成


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


 if (vm.$options.el) {

      vm.$mount(vm.$options.el)

    }

  }

}


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


new Vue({

  router,

  store,

  i18n,

  render: h => h(App)

}).$mount('#app')


以上就是Vue实例的初始化过程。

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “vue”,获取更多资料,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容