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

2019-10-13 04:27 来源:未知

第二阶段:创建 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) } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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) }

1
2
3
4
5
6
7
8
9
10
11
    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 ) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    // 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 }

1
2
3
4
5
6
  /* 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` 阶段完成

1
2
3
4
5
6
7
8
9
10
11
  // 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) } } }

1
2
3
4
5
    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')

1
2
3
4
5
6
new Vue({
  router,
  store,
  i18n,
  render: h => h(App)
}).$mount('#app')

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

1 赞 收藏 评论

彩民之家高手论坛 1

/* istanbul ignore if */

initLifecycle(vm)
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

该方法主要就是给vm对象添加了$parent、$root、$children属性,以及一些其它的生命周期相关的标识。

options.abstract用于判断是否是抽象组件,组件的父子关系建立会跳过抽象组件,抽象组件比如keep-alive、transition等。所有的子组件$root都指向顶级组件。

创建Vue实例的两步

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

JavaScript

import Vue from ‘vue' new Vue(options)

1
2
import Vue from ‘vue'
new Vue(options)

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

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

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

好了,我们一开始不需要关心那么多边边角角,直接从23行代码开始看,因为大部分情况下是走了这条分支,也就是执行了下面的代码:

因为最近我们组内有个分享主题,即vue2的源码学习分享,我们几个人分别分享几个不同部分,但是虽然我们的分工是每个人分享不同部分,但是源码里面并没有一个具体的分块,所以不管学习那一部分,都需要了解学习其他部分,因此想着按照每个js文件去学习是不大现实的,所以就通过一个小实例,跟着这个小实例一步一步的去源码,通过在网上看了很多的文章,整理这一篇学习笔记,即

第一阶段:创建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

JavaScript

import Vue from './runtime/index' export default Vue

1
2
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  省略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) // 构造函数有用的只有这一行代码,是不是很简单,至于这一行代码具体做了什么,在第二阶段我们详细讲解。 }

1
2
3
4
5
6
7
8
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)

1
2
3
4
5
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) { // 省略,这部分我们会在第二阶段讲解 } }

1
2
3
4
5
6
export function initMixin (Vue: Class<Component>) {
  // 就是这里,添加了一个方法
  Vue.prototype._init = function (options?: Object) {
    // 省略,这部分我们会在第二阶段讲解
  }
}

另外的几个同样都是在 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 来添加的,那么我们直接看看这个函数的样子:

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) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 //...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//构造函数
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.prototype.$emit = function (event: string): Component {}

通过一个demo实例看vue的生命周期

此次分享,旨在通过一个简单的小栗子,和大家一起学习从vm创建,到显示到页面上都经历了哪些过程。

如下栗子:

<div id="app">
  <p>{{message}}</p>
</div>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    data: {
      message: 'this is a vue test'
    }
  })
</script>

以上栗子会经过如下过程:

彩民之家高手论坛 2

lifecycle1.png

那么该栗子中的el和message在这些生命周期钩子中的状态如何?我们可以通过在浏览器打印出来看看,

总结为一张图就是:

彩民之家高手论坛 3

p1.png

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

2018/08/02 · JavaScript · Vue

原文出处: lihongxun945   

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

彩民之家高手论坛 4

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

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

彩民之家高手论坛 5

// update base extend options

vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode

    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

从mountComponent中我们知道创建Watcher对象先于vm._isMounted = true。所以这里的vm._isMounted还是false,不会调用beforeUpdate钩子函数。

下面会调用vm.patch,在这一步之前,页面的dom还没有真正渲染。该方法包括真实dom的创建、虚拟dom的diff修改、dom的销毁等。

Vue.prototype.__patch定义在src/platform/web/runtime/index.js

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

initEvents(vm)
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

该方法初始化事件相关的属性

那么我们下面就要受好奇心的驱动,来看看 Vue构造函数 是什么样的?

然后,就会调用我们的created钩子函数。

我们看到create阶段,基本就是对传入数据的格式化、数据的双向绑定、以及一些属性的初始化。

})

先看下resolveConstructorOptions
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super属性,说明Ctor是通过Vue.extend()方法创建的子类
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

这里的Ctor就是vm.constructor也就是Vue对象,在/src/core/global-api/index文件中,会给Vue添加了一些全局的属性或方法。

Vue.options = Object.create(null)
// Vue.options.components、Vue.options.directives、Vue.options.filters
config._assetTypes.forEach(type => {
  Vue.options[type   's'] = Object.create(null)
})

// Vue.options._base
Vue.options._base = Vue

// Vue.options.components.KeepAlive
extend(Vue.options.components, builtInComponents)

所以,这里打印一下Ctor.options,如下所示:

Ctor.options = {
  components: {
    KeepAlive,
    Transition,
    TransitionGroup
  },
  directives: {
    model,
    show
  },
  filters: {},
  _base: Vue
}

Ctor.super是在调用Vue.extend时,才会添加的属性,这里先直接跳过。所以mergeOptions的第一个参数就是上面的Ctor.options,第二个参数是我们传入的options,第三个参数是当前对象vm。所以我们再看下mergeOptions方法:

renderMixin(Vue)

然后会调用beforeCreate钩子函数。

我们来看一下钩子函数的执行,callHook()方法定义在src/core/instance/lifecycle.js中,如下:

export function callHook (vm: Component, hook: string) {
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i  ) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:'   hook)
  }
}

其实就是把钩子函数执行一下,其他钩子调用时也一样。

接着往下看

}

initInjections(vm)和initProvide(vm)
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

export function initInjections (vm: Component) {
  const inject: any = vm.$options.inject
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    // isArray here
    const isArray = Array.isArray(inject)
    const keys = isArray
      ? inject
      : hasSymbol
        ? Reflect.ownKeys(inject)
        : Object.keys(inject)

    for (let i = 0; i < keys.length; i  ) {
      const key = keys[i]
      const provideKey = isArray ? key : inject[key]
      let source = vm
      while (source) {
        if (source._provided && provideKey in source._provided) {
          if (process.env.NODE_ENV !== 'production') {
            defineReactive(vm, key, source._provided[provideKey], () => {
              warn(
                `Avoid mutating an injected value directly since the changes will be `  
                `overwritten whenever the provided component re-renders. `  
                `injection being mutated: "${key}"`,
                vm
              )
            })
          } else {
            defineReactive(vm, key, source._provided[provideKey])
          }
          break
        }
        source = source.$parent
      }
    }
  }
}

这两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察。简单的例子如下:

<div id="app">
    <p>{{message}}</p>
    <child></child>
</div>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            message: '第一个vue实例'
        },
        components: {
            child: {
                template: "<div>{{a}}</div>",
                inject: ['a']
            }
        },
        provide: {
            a: 'a'
        }
    })
</script>

Vue.prototype.$mount

initState(vm)
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

这里主要就是操作数据了,props、methods、data、computed、watch,从这里开始就涉及到了Observer、Dep和Watcher,不多做讲解。

到这一步,我们看看我们的vm对象变成了什么样:

// _init
vm._uid = 0
vm._isVue = true
vm.$options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue,
    el: '#app',
    data: function mergedInstanceDataFn(){}
}
vm._renderProxy = vm
vm._self = vm

// initLifecycle
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

// initEvents   
vm._events = Object.create(null)
vm._hasHookEvent = false

// initRender
vm.$vnode = null
vm._vnode = null
vm._staticTrees = null
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 在 initState 中添加的属性
vm._watchers = []
vm._data
vm.message

可以打印一下此时的vm

Transition,

源码层面

以上我们是从应用层面的生命钩子去了解了vue的生命周期的一些情况,那么在源码里,是如何实现的?

首先是创建对象,当然要从构造函数看起,构造函数在src/core/instance/index.js中。

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)
}

我们看到,它首先判断了是不是通过new关键词创建,然后调用了this._init(options)。_init函数是在src/core/instance/init.js中添加的。我们先把整个函数都拿出来,然后看看每一步都做了什么。

Vue.version = 'VERSION'

mountComponent
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')     //  调用beforeMount钩子

  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')     // 调用mounted钩子
  }
  return vm
}

上面的代码我简单的做了一些精简。可以看到首先调用了beforeMount钩子函数,新建了一个Watcher对象,绑定在vm._watcher上,之后就是判断如果vm.$vnode == null,则设置vm._isMounted = true并调用mounted钩子函数,最后返回vm对象。

接着简单看下Watcher,

打开src/core/observer/watcher.js

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    ...
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" `  
          'Watcher only accepts simple dot-delimited paths. '  
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }

    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

get: isServerRendering

vm._render

updateComponent中调用了vm._render()函数,该方法在src/core/instance/render.js中。

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options

    ...
    if (staticRenderFns && !vm._staticTrees) {
      vm._staticTrees = []
    }

    vm.$vnode = _parentVnode
    // render self
    let vnode

    vnode = render.call(vm._renderProxy, vm.$createElement)
    ...

    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function '  
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
 // set parent
    vnode.parent = _parentVnode
    return vnode
  }

在该方法中,其实主要就是调用了vm.$options.render方法,我们再拿出render方法,看看它都干了什么。

render = function () {
    with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}

函数调用过程中的this,是vm._renderProxy,是一个Proxy代理对象或vm本身。我们暂且把它当做vm本身。

_c是(a, b, c, d) => createElement(vm, a, b, c, d, false)。我们简单说一下createElement干了什么。a是要创建的标签名,这里是div。接着b是data,也就是模板解析时,添加到div上的属性等。c是子元素数组,所以这里又调用了_c来创建一个p标签。

_v是createTextVNode,也就是创建一个文本结点。_s是_toString,也就是把message转换为字符串,在这里,因为有with(this),所以message传入的就是我们data中定义的第一个vue实例。

所以,从上面可以看出,render函数返回的是一个VNode对象,也就是我们的虚拟dom对象。它的返回值,将作为vm._update的第一个参数。我们接着看该函数,返回src/core/instance/lifecycle.js

directives: {},

$mount

打开src/platforms/web/web-runtime-with-compiler.js。

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:'   template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

首先,通过mount = Vue.prototype.$mount保存之前定义的$mount方法,然后重写。

这里的query可以理解为document.querySelector,只不过内部判断了一下el是不是字符串,不是的话就直接返回,所以我们的el也可以直接传入dom元素。

之后判断是否有render函数,如果有就不做处理直接执行mount.call(this, el, hydrating)。如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。

compileToFunctions是对我们最后生成的模板进行解析,生成render。这里的内容也比较多,简单说一下:

该方法创建的地方在src/compiler/index.js的createCompiler中。

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}


export function createCompiler (baseOptions: CompilerOptions) {
  const functionCompileCache: {
    [key: string]: CompiledFunctionResult;
  } = Object.create(null)

  function compile (
    template: string,
    options?: CompilerOptions
  ): CompiledResult {
    ...
    const compiled = baseCompile(template, finalOptions)
    ...
    return compiled
  }

  function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = options || {}
    ...
    // compile
    const compiled = compile(template, options)
    ...
    return (functionCompileCache[key] = res)
  }

  return {
    compile,
    compileToFunctions
  }
}

compileToFunctions中调用了compile,compile中调用了baseCompile。主要的操作就是baseCompile中的三步。

第一步,const ast = parse(template.trim(), options)。这里是解析template,生成ast。我们的例子生成的ast如下:

{
  type: 1,
  tag: 'div',
  plain: false,
  parent: undefined,
  attrs: [{name:'id', value: '"app"'}],
  attrsList: [{name:'id', value: 'app'}],
  attrsMap: {id: 'app'},
  children: [{
    type: 1,
    tag: 'p',
    plain: true,
    parent: ast,
    attrs: [],
    attrsList: [],
    attrsMap: {},
    children: [{
      expression: "_s(message)",
      text: "{{message}}",
      type: 2
    }]
}

第二步,optimize(ast, options)主要是对ast进行优化,分析出静态不变的内容部分,增加了部分属性:

{
  type: 1,
  tag: 'div',
  plain: false,
  parent: undefined,
  attrs: [{name:'id', value: '"app"'}],
  attrsList: [{name:'id', value: 'app'}],
  attrsMap: {id: 'app'},
  static: false,
  staticRoot: false,
  children: [{
    type: 1,
    tag: 'p',
    plain: true,
    parent: ast,
    attrs: [],
    attrsList: [],
    attrsMap: {},
    static: false,
    staticRoot: false,
    children: [{
      expression: "_s(message)",
      text: "{{message}}",
      type: 2,
      static: false
    }]
  }

因为我们这里只有一个动态的{{message}},所以static和staticRoot都是false。

最后一步,code = generate(ast, options),就是根据ast生成render函数和staticRenderFns数组。

最后生成的render如下:

render = function () {
    with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}

最后生成的staticRenderFns如下:

staticRenderFns = function () {
    with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
}

在src/core/instance/render.js中,可以找到这里和render内返回值调用一一对应的函数。

Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = _toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots

从上面的内容,我们可以知道其实template最终还是转换为render函数,这也是官方文档中所说的render函数更加底层。

前面保存了mount = Vue.prototype.$mount,最后又调用了mount方法,我们来看看它干了什么。

打开src/platforms/web/web-runtime.js。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

这里仅仅是返回了mountComponent的执行结果,跟着代码的步伐,我们又回到了src/core/instance/lifecycle.js。

},

updated钩子

updated钩子是在observer中执行,见src/core/observer/scheduler.js

extend(Ctor.extendOptions, modifiedOptions)

initRender(vm)
export function initRender (vm: Component) {
  vm.$vnode = null 
  vm._vnode = null 
  vm._staticTrees = null
  const parentVnode = vm.$options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

这里给vm添加了一些虚拟dom、slot等相关的属性和方法。

// check if there are any late-modified/attached options (#4976)

this._init

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid  

    let startTag, endTag
    // 性能统计相关
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    //设置vm._isVue为true(监听对象变化时用于过滤vm)
    vm._isVue = true

    //_isComponent是内部创建子组件时才会添加为true的属性,我们的小栗子会直接走到了else里面。
    if (options && options._isComponent) {
      // 内部使用Vnode部分使用
      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')

    // 性能相关
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
 }

mergeOptions用于合并两个对象,不同于Object.assign的简单合并,它还对数据还进行了一系列的操作,且源码中多处用到该方法,所以后面会详细讲解这个方法。resolveConstructorOptions方法的作用是合并构造器及构造器父级上定义的options。

filters: {},

ergeOptions

mergeOptions是Vue中处理属性的合并策略的地方。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 如果有options.components,则判断是否组件名是否合法
    checkComponents(child)
  }
  // 格式化child的props
  normalizeProps(child)
  // 格式化child的directives
  normalizeDirectives(child)
  // options.extends
  const extendsFrom = child.extends 
  if (extendsFrom) {
    parent = typeof extendsFrom === 'function'
      ? mergeOptions(parent, extendsFrom.options, vm)
      : mergeOptions(parent, extendsFrom, vm)
  }
  // options.mixins
  if (child.mixins) { 
    for (let i = 0, l = child.mixins.length; i < l; i  ) {
      let mixin = child.mixins[i]
      if (mixin.prototype instanceof Vue) {
        mixin = mixin.options
      }
      parent = mergeOptions(parent, mixin, vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

上面和components、props、directives、extends、mixins相关的内容我们暂且忽略

我们主要看一下data属性的合并策略,是也是Vue内置的,如下:

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  for (let i = 0; i < keys.length; i  ) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

strats.data = function (    
parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (!childVal) {
      return parentVal
    }
    if (typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function '  
        'that returns a per-instance value in component '  
        'definitions.',
        vm
      )
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(
        childVal.call(this),
        parentVal.call(this)
      )
    }
  } else if (parentVal || childVal) {     // 我们的栗子会走到这里
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

这里vm且data都不为空,所以会走到else if,返回的是mergedInstanceDataFn方法。关于mergedInstanceDataFn方法,我们都知道,子组件中定义data时,必须是一个函数,这里简单的判断了是函数就执行,不是就返回自身的值。然后通过mergeData去合并,其实就是递归把defaultData合并到instanceData,并观察。

最后合并之后的vm.$option如下:

vm.$option = {
  components: {
    KeepAlive,
    Transition,
    TransitionGroup
  },
  directives: {
    model,
    show
  },
  filters: {},
  _base: Vue,
  el: '#app',
  data: function mergedInstanceDataFn(){}
}

回到我们的_init接着放下看,之后如果是开发环境,则vm._renderProxy值为一个Proxy代理对象,生产环境就是vm自身,这里不展开赘述。

接着就是一系列的操作,我们一个一个来看。

Vue.filter = function(){}

_base: Vue

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

resolveConstructorOptions(vm.constructor),

import { renderMixin } from './render'

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

}

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

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

options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)

这里要注意的是Vue.options 的变化。

lifecycleMixin(Vue)

model,

},

}

Vue.prototype.$nextTick = function (fn: Function) {}

第22行,resolveConstructorOptions 方法直接返回了 Vue.options。也就是说,传递给 mergeOptions 方法的第一个参数其实是 Vue.options。那么,实际上原来的代码就变成了下面这样:

}

Vue.prototype.$destroy = function () {}

vm._uid = uid

callHook(vm, 'beforeCreate')

2、在 Vue 上挂载 compile

Vue.prototype._l = function(){}

Vue.prototype._q = looseEqual

Vue.prototype._n = toNumber

为什么要使用 mergeOptions 方法呢? 是为了 合并策略, 对于子组件和父组件如果有相同的属性(option)时要进行合并,相关文章:

import { lifecycleMixin } from './lifecycle'

Vue.cid = 0

b: [1, 2, 3]

directives: {

// 调用Vue构造函数时传入的参数选项 options

Vue.prototype.$watch = function(){}

options.components[options.name] = Ctor

}

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

Vue.config.getTagNamespace = getTagNamespace

options || {},

Vue.nextTick = util.nextTick

http://www.tuicool.com/articles/UbqqAfY

Object.defineProperty(Vue.prototype, '$isServer', { //为 Vue.prototype 添加$isServer属性

最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js 了,该文件做了两件事:
1、缓存来自 web-runtime.js 文件的 $mount 函数
const mount = Vue.prototype.$mount

if (process.env.NODE_ENV !== 'production' && // 这个 if 判断,是当你不用new操作符来实例化Vue构造函数时,会爆出警告

Ctor.superOptions = superOptions

Vue.prototype.$on = function (event: string, fn: Function): Component {}

3、web-runtime.js 主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js 给Vue的 $mount 方法添加 compiler 编译器,支持 template。

TransitionGroup

import { stateMixin } from './state'

data: {

}

Vue.config.mustUseProp = mustUseProp

export function resolveConstructorOptions (Ctor: Class<Component>) { //ctor 就是 VUE 构造函数

Vue.prototype.$forceUpdate = function () {}

// expose real self

},

vue 框架号称五分钟就能上手,半小时就能精通,这是因为其使用非常简单,就像下面一样:
let vm = new Vue({

Vue.set = set

Vue.prototype.$off = function (event?: string, fn?: Function): Component {}

在最开始,我传递了两个选项 el 以及 data ,很简单,官网上也是这样写的。
你肯定注意到了,我使用了 new 操作符。这就很自然的想到,Vue 就是一个构造函数,vm是 Vue构造函数 生成的实例,我们的配置项是传入构造函数的参数,是一个包括 el 属性 和 data属性的对象;

return options

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

// 这是原来的代码

initGlobalAPI(Vue)

Vue.prototype._f = function resolveFilter (id) {}

TransitionGroup

this._init(options)

)

}

KeepAlive

a: 1,

KeepAlive,

data: {

Vue.delete = del

eventsMixin(Vue)

{

下一个就是 web-runtime.js 文件了,web-runtime.js 文件主要做了三件事儿:

Vue.util = util

Vue.prototype._e = createEmptyVNode

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

Vue.prototype.$data

const superOptions = resolveConstructorOptions(Ctor.super)

Vue.prototype._m = function(){}

if (options && options._isComponent) {

// 安装平台特定的 指令 和 组件

好了,我们再回过头来看 this._init() 方法,_init() 方法就是Vue调用的第一个方法,然后将我们的参数 options 传了过去。_init() 是在 node_modulesvuesrccoreinstanceinit.js 文件中被声明的:
Vue.prototype._init = function (options?: Object) {

Vue.mixin

filters: {},

// super option changed,

initGlobalAPI() 的作用是在 Vue 构造函数上挂载静态属性和方法,Vue 在经过 initGlobalAPI 之后,会变成这样:

vm

},

resolveConstructorOptions(vm.constructor),

if (vm.$options.el) {

show

initMixin(Vue)

上面的代码调用了五个方法,这五个方法都是把Vue构造函数作为参数传入,其目的都是在 Vue .prototype 上挂载方法或属性,这个概念很好理解,我们在js 的原型链继承的学习中,经常把属性和方法丢到构造函数的原型上作为公有的属性和方法。

Vue.prototype.$set = set

if (modifiedOptions) {

KeepAlive,

lifecycleMixin(Vue)

}

// internal component options needs special treatment.

},

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}

}

initLifecycle(vm)

Vue.directive = function(){}

options || {},

}

options || {},

Vue.config

Vue.prototype.$delete = del

}

Vue.prototype._b = function(){}

vm._renderProxy = vm

this._init(options) // 主要就是这一句,

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

vm.$options = mergeOptions(

Vue.prototype._i = looseIndexOf

el: '#app',

vm._isVue = true

components: {

model,

!(this instanceof Vue)) {

eventsMixin(Vue)

}

const modifiedOptions = resolveModifiedOptions(Ctor)

!(this instanceof Vue)) {

// 实际上传过去的参数是下面这些

Vue.prototype.patch

directives: {

至此,我们算是还原了 Vue 构造函数,总结一下:
1、Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的

Vue.prototype._mount = function(){}

{

el: '#app',

Vue.options = {

resolveConstructorOptions(vm.constructor),

const cachedSuperOptions = Ctor.superOptions

_base: Vue

Vue.version = 'VERSION' // 在VUE 身上挂载了 version的静态属性

show

经过 web-runtime.js 文件之后,Vue 变成下面这个样子:

Vue.prototype._updateFromParent = function(){}

}

// initMixin(Vue) src/core/instance/init.js **************************************************

这里是执行了 mergeOptions 函数,并将返回值赋值给 vm.$options 属性。 mergeOptions 函数接受三个参数,分别是 resolveContructorOptions方法, 我们调用 vue 构造函数传入的配置对象(如果没有就是空对象),以及 vm 实例 本身。

function Vue (options) {

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

}

// stateMixin(Vue) src/core/instance/state.js **************************************************

Vue.use

/* istanbul ignore if */

Vue.options = {

Vue.prototype.$isServer

Vue.config.isReservedTag = isReservedTag

}

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

} else { // 大部分情况下是走了这个分支,也是vue第一步要做的事情,使用mergeOptions来合并参数选项

vm.$options = mergeOptions(

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

vm._self = vm

let options = Ctor.options // vue 构造函数身上的 options 属性

components: {

import { isServerRendering } from 'core/util/env'

import { eventsMixin } from './events'

import { warn } from '../util/index'

stateMixin(Vue)

2、Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的

}

vm._name = formatComponentName(vm, false)

const vm: Component = this

filters: {},

mark(startTag)

if (options.name) {

initEvents(vm)

let startTag, endTag

// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************

Vue.prototype._v = createTextVNode

// Vue.options

2、Vue.options.directives 和 Vue.options.components 安装平台特有的指令和组件

}

)

vm

经过上面5个方法对Vue构造函数的处理,vm实例上就可以使用这些属性和方法了。其实在其他地方,Vue 构造函数也被处理了:在src/core/index.js 文件中:
import Vue from './instance/index'

callHook(vm, 'created')

// 安装平台特定的utils

Vue.extend

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

上面 compileToFunctions 函数可以将模板 template 编译为render函数。

// this

Vue.prototype._k = function(){}

Vue.component = function(){}

initRender(vm)

在构造函数里面,我们要关心的是 this._init( options ) , 稍微我会详细的来讲,我们先看 node_modulesvuesrccoreinstanceindex.js 文件中的第16行~20行:
initMixin(Vue)

发现了吧,Vue 的确是一个构造函数,和你平时使用的 Array, Object 等普普通通的构造函数,没有本质的区别。

Vue.compile = compileToFunctions

b: [1, 2, 3]

// a flag to avoid this being observed

// eventsMixin(Vue) src/core/instance/events.js **************************************************

Vue.prototype.$once = function (event: string, fn: Function): Component {}

/* istanbul ignore else */

在 node_modulesvuesrccoreinstanceindex.js 文件里面,是下面的代码:
import { initMixin } from './init'

3、在 Vue.prototype 上定义 patch 和 $mount

})

stateMixin(Vue)

function Vue (options) {

_base: Vue

vm.$options = mergeOptions(

我们先看 resovleContructorOptions 方法, 传入的参数是 vm.constructor 。 vm.constructor 代表的是啥? const vm: Component = this 人家_init() 函数第一行就定义了,是指向_init() 函数内部的this, _init( ) 函数是 Vue.prototype上的一个方法,所以在其身上调用的时候,this 指向本身 Vue.prototype, 那么 vm.constructor 也就是指向 Vue 构造函数.

Vue.prototype._o = function(){}

// need to resolve new options.

// merge options

1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法

export default V

if (superOptions !== cachedSuperOptions) {

mark(endTag)

},

Vue.config.isUnknownElement = isUnknownElement

vm.$options = mergeOptions(

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

// a uid

Transition,

a: 1,

// renderMixin(Vue) src/core/instance/render.js **************************************************

renderMixin(Vue)

components: {

initProxy(vm)

Vue.prototype._t = function(){}

initState(vm)

不用害怕,我带你捋一捋,我们首先关注第8行,我摘抄出来:

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

},

initInternalComponent(vm, options)

export default Vue

Vue.prototype._render = function (): VNode {}

)

// optimize internal component instantiation

vm

if (Ctor.super) { // 判断是否定义了 Vue.super ,这个是用来处理继承的,我们后续再讲

)

}

}

Vue.prototype._s = _toString

vm

} else {

版权声明:本文由彩民之家高手论坛发布于前端知识,转载请注明出处:vue2.x源码解析系列二: Vue组件初始化过程概要【彩