vue源码分析(三) 整体架构

10/3/2018 vue源码分析面试

# 1. 概述

​ 在上一章——vue源码分析(二) 源码构建 (opens new window),中我们分析项目的入口,接下来我们从项目入口开始分析 Vue 源码的整体结构。

# 2. 项目入口

​ 我们以 npm run dev 为例,执行这行命令其实就是执行 package.json 中的 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"这行。我们可以上一章的分析清楚的知道最终执行的是 config.js 文件,调用的是 genConfig(process.env.TARGET)函数。

web-full-dev ,它的 entryresolve('web/entry-runtime-with-compiler.js') ,分析到这里,我们知道了项目的入口是 web/entry-runtime-with-compiler.js,下面我们看看这个文件。

​ 源码目录: src/platforms/web/entry-runtime-with-compiler.js

import Vue from './runtime/index'
/*...*/

const mount = Vue.prototype.$mount // 缓存 runtime 中的 $mount 方法
Vue.prototype.$mount = function () {
  
  /**
   * el:真实的dom
   * hydrating:undefined
   * mount:定义在 src/platforms/web/runtime/index.js 中
   */
  return mount.call(this, el, hydrating)
} // 重写 $mount 方法
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 从上面的代码我们可以知道,最终执行的是 const mount = Vue.prototype.$mount,即 Vue 原型方法 $mount,他定义在 ./runtime/index ,如下:

​ 源码目录: src/platforms/web/runtime/index.js

import Vue from 'core/index'
 /*...*/
// 原型上声明的 $mount方法在,这个方法会被runtime only版本和runtime compiler版本中复用
Vue.prototype.$mount = function (
  el?: string | Element, // 真实的dom 或者 是string
  hydrating?: boolean // 新的虚拟dom vnode
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
 /*...*/
1
2
3
4
5
6
7
8
9
10
11

​ 这里是给 Vue 原型上添加 $mount 方法,我们可一得到 Vue 构造函数是通过 import Vue from 'core/index' 导入的,所以 Vue 构造函数定义在 'core/index' ,下面我们分析一下 Vue 的构造函数。

# 3.Vue构造函数

​ 源码目录: src/core/index.js

import Vue from './instance/index'
 /*...*/

initGlobalAPI(Vue) // 初始化全局的API
 /*...*/
1
2
3
4
5

​ 通过这段代码我们知道,Vue 构造函数最终被定义在核心模块中,与平台无关。

说明: initGlobalAPI 函数我们下一章详细分析,这里就不多做说明!

​ 定义如下:

​ 源码目录: src/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// Vue 构造函数
function Vue (options) {
  // 生产环境下,没有使用 new 创建实例会报下面警告
  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) // 初始化Vue
stateMixin(Vue) // 数据绑定
eventsMixin(Vue) // 初始化事件
lifecycleMixin(Vue) // 初始化vue 生命周期: 更新 销毁 
renderMixin(Vue) // 初始化渲染的函数

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

​ 这段源码主要的做了一下几件事:

​ (1)、定义Vue 构造函数。

​ (2)、初始化Vue。

​ (3)、数据绑定 。

​ (4)、初始化事件。

​ (5)、初始化vue 生命周期 。

​ (6)、初始化渲染的函数 。

​ 源码分析到这里,我们终于找到了Vue的构造函数的定义,在分析Vue构造函数之前,我们先带着一个问题去分析源码:new Vue (opens new window) 发生了什么?

# 3.1 new

​ 在 JavaScriptnew 主要做了一下几件事:

​ (1)、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。

​ (2)、属性和方法被加入到 this 引用的对象中。

​ (3)、新创建的对象由 this 所引用,并且最后隐式的返回 this

// Javascript的new关键字主要的作用是继承
// new一共经历4个阶段
// 1、创建一个空对象
var obj = new Object();

// 2、设置原型链
// 此时便建立了obj对象的原型链
obj._proto_ = Object.prototype;

// 3、让Func的this指向obj,并执行Func函数体
Object.call(obj);

// 4、判断Func的返回值类型
// 如果是值类型,返回obj;
// 如果是引用类型,返回这个引用类型的对象。
return typeof result === 'obj'? result : obj;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3.2 Vue

​ 源码目录: src/instance/index.js

// Vue 构造函数
function Vue (options) {
  // 生产环境下,没有使用 new 创建实例会报下面警告
  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
9
10

​ 可以看到 Vue 构造函数是一个很简洁的function工厂模式声明的一个构造函数, Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法。

# 4.初始化

​ 源码目录:src/core/instance/init.js

/**
 * 初始化vue
 * @param {Vue构造器} Vue 
 */
export function initMixin (Vue: Class<Component>) {
  //初始化函数
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    /* 
    * 测试代码性能
    * 参考:https://segmentfault.com/a/1190000014479800
    */
    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
    // 避免被响应式的标识
    // 这里可以暂时理解新建observer实例就是让数据响应式
    vm._isVue = true
    // merge options
    // 有子组件时,options._isComponent才会为true
    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 { // 根Vue实例执行到这里
      // EMPHASIS:(重点分析:合并options)传入的options和vue自身的options进行合并保存到vm.$options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // 解析constructor上的options属性的
        options || {},
        vm
      )
    }
    /* 
    * istanbul ignore else 
    * 参考:https://segmentfault.com/a/1190000014824359
    */
    if (process.env.NODE_ENV !== 'production') {
      // 初始化代理监听
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm // 开放真实的self
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化渲染
    // EMPHASIS:(生命周期:beforeCreate)
    callHook(vm, 'beforeCreate') // 触发 beforeCreate 钩子函数
    /**
     * 在data / props之前解决注入问题
     * 初始化 inject
     */
    initInjections(vm) // resolve injections before data/props
    // 初始化props属性、data属性、methods属性、computed属性、watch属性
    initState(vm)
    /**
     * 在data / props初始化后解决注入问题
     * 选项应该是一个对象或返回一个对象的函数
     * 该对象包含可注入其子孙的属性,用于组件之间通信
     */
    initProvide(vm) // resolve provide after data/props
    // EMPHASIS:(生命周期:created)
    callHook(vm, 'created') // 触发 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) {
      /**
       * 手动挂载
       * 在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上
       * new Vue时,el和 $mount 并没有本质上的不同
       */
      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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

Vue 初始化主要就干了几件事情,合并配置初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher初始化 provide/inject 等,这里就不对它们做详细分析了,我们将在后面章节中进行详细分析。

​ 分析到这里我们 Vue 框架的代码整理架构就分析完了。

# 5.总结

vue源码整体架构