Vue 的构建和初始化

构建版本

通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候会询问我们用 Runtime Only 版本的还是 Runtime + Compiler 版本

Runtime Only

编译时转化,离线编译。

Runtime Only 版本通常需要借助如 webpack 的 vue-loader工具 把 .vue 文件 编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更 轻量,性能更优。

Runtime + Compiler

运行时,客户端编译

如果没对代码做预编译,但又使用了 Vue 的 template 属性 并传入一个字符串,则需要在客户端编译模板,即运行时编译。因为在 Vue2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本,很显然,这个编译过程对性能有一定的损耗。所以我们通常更推荐使用 Runtime- Only 的 Vue.js

初始化

当代码执行 import Vue from ‘vue’ 的时候,是从这个入口执行代码来初始化 Vue。 Vue 实际 是一个用 Function 实现的类,只能通过 new Vue() 实例化。

为什么 Vue 不用 ES6 的 Class 去实现呢?

在 Vue 初始化文件中,有很多 xxxMin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法,Vue 按功能把这些扩展分散都多个模块中去实现,而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。 这么做的好处就是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。

Vue.js 在整改初始化过程中,除了给它的原型 prototype 上扩展方法,还会通过 initGlobal API 给 Vue 这个对象本身扩展全局的静态方法,即 Vue 的全局 API

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

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

Vue 初始化主要就干了以下几件事情:
合并配置、初始化生命周期、初始化事件中心、初始化渲染、初始化 data、props、computed、watcher等等。在初始化的最后,如果有检测到 el 属性,则调用 vm.$mount 方法挂在 vm, 挂载的目标就是把模版渲染成最终的 DOM

vue init

new Vue 发生了什么 - init

js
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
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
vm._uid = uid++

let startTag, endTag

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vuw-perf-end:${vm._uid}`
mark(startTag)
}

vm._isVue = true
// merge options
if (options && options._isComponents) {
// 每个子组件初始化时走这里,这里只做了一些性能优化,
// 将组建配置对象上的一些深层次属性 放到 vm.$options 选项中,以提高代码的执行效率
initInternalComponents(vm, options)
} else {
/**
* 初始化根组件时走到这里,合并 Vue 的全局配置到根组件的局部配置,如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中
* 至于每个子组件的选项合并则发生在两个地方:
* 1. Vue.component 方法注册的全局组件在注册时做了选项合并
* 2. { component: { xx }} 方式注册的局部组件在执行编译器生成的 render 函数时做了选项合并,包括根组件中的 components 配置
*
*/
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}

if ( process.env.NODE_ENV !== 'production' ) {
// 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
initProxy(vm)
} else {
vm._renderProxy = vm
}

vm._self = vm

// 初始化组件实例关系属性,比如 $parent $children $root $refs 等
initLifecycle(vm)
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件, 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm)
// 解析组件的插槽信息,得到 vm.$slot, 处理渲染函数,得到 vm.$createElement 方法,即 h 函数,将 render函数转为 vnode 的方法
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置,得到 result[key] = val 形式的配置对象,然后对结果数据进行相应式处理,并代理每个 key 到 vm 实例
initInjections(vm)
// 数据响应式的重点,处理 props、method、data、computed、watch
initState(vm)
// 解析组件配置项上的 provide 对象,挂载到 vm._provided 属性上
initProvide(vm)

// 调用 created 钩子函数
callHook(vm, 'created')

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果 配置项上 有 el ,则自动调用 $mount 方法,如果没有需要手动调用
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

}

Vue 初始化主要就干了几件事情,合并配置、初始化生命周期、初始化事件、初始化渲染、执行 beforeCrate 钩子、初始化依赖注入内容、初始化 prop、methods、data、computed、watcher,解析组件配置上的 provide 对象,执行 created 钩子,最后 mount 挂载真实 DOM

Vue 实例挂载 - $mount

Vue 中是通过 $mount 实例方法去挂在 vm 的,$mount 方法的实现是和平台、构建方式相关

js
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
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el?: string | Element, hydration?: boolean): Components {
el = el && query(el)

if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead`
)
return this
}

const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string'){
if (template.charAt(0) === '#') {
template = idToTemplate(template)

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) {
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewLines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 原先原型上的 $mount 方法
return mount.call(this, el, hydrating)
}

首先,对 el 做了现在, Vue 不能挂在在 body、html 这样的根节点上,接下来的关键逻辑 — 如果没有定义 render 方法,则会把 el 或 template 字符串转换成 render 方法。在 vue2.0 版本中,所以 vue 的组件渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或 template 属性。最终都会转换成 render 方法, 那么这个过程是 vue 的一个 “在线编译” 的过程,他是调用 compileToFunctions 方法实现的。最后调用原先原型上的 $mount 方法挂载

js
1
2
3
4
Vue.prototype.$mount = function(el?: string | Element, hydrating?: boolean): Component {
el = el && inBrowser ? query(el): undefined
return mountComponents(this, el, hydrating)
}

*$mount 方法支持传入 2 个参数,第一个是 el, 它表示挂载的元素,可以是字符串,也可以说 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。
*$mount 方式的实质是调用 mountComponent 方法

js
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
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

mountComponent 核心就是先实例化一个渲染 watcher,在它的回调函数中调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成 虚拟 Node,最终调用 vm._update 更新 DOM。 Watcher 在这里起到两个作用,一个是初始化的时候会执行毁掉你函数,另一个是当 vm 实例中的监测数据发生变化时执行的回调呢函数。
函数最后判断为根节点的时候 设置 vm._isMounted 为true,表示这个实例已经挂载了,同时执行 mounted 钩子函数。
这里 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 null 则表示当前是根 vue 实例

mountComponent 会完成整个渲染工作,包含最核心的 2 个 方法: vm._render 和 vm._update

渲染 DOM - render

Vue 的 _render 方法是实例的一个私有方法,他用来把实例渲染成一个虚拟 Node

js
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
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options

if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots
)
}

// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
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
}

vm._render 最终是通过 执行 createElement 方法并返回的是 vnode,它是一个虚拟 node

生成虚拟 DOM - createElement

Vue 利用 createElement 方法创建 VNode,createElement 方法实际上是对 _createElement 方法的封装

js
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
100
101
102
103
104
// src\core\vdom\create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}

_createElement 方法有 5个参数:

  • context 表示 VNode 的上下文环境,它是 Component 类型
  • tag 表示标签,它可以是一个 字符串,也可以是一个 Component
  • data 表示 VNode 的数据,它是一个 VNodeData 类型
  • children 表示当前 VNode 的子节点,它是任意类型的,它接下来需要被规范为标准版 的 VNode 数组
  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考 render 函数是编译生成的还是用户手写的,经过对 children 的规范化,children 变成了一个类型为 VNode 的 Array

每个 VNode 有 children,children 的每个元素也是一个 VNode,这样就形成了一个 Vnode tree,它很好的描述了我们的 DOM tree

回到 mountComponent 函数的过程,我们已经知道 vm._render 是如何创建了一个 VNode,接下来就是要把这个 Vnode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成。

渲染真实 DOM - update

Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。_update 方法的作用是把 VNode 渲染成真实的 DOM

js
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
// src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// 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
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}

_update 的核心就是调用 vm.patch 方法,这个方法实际上在不同的平台上有不同的实现方式。比如,服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器渲染中,它指向了 patch 方法

js
1
2
3
4
5
6
7
8
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules form 'web/runtime/modules/index'

const module = platformModules.contact(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

该方法的定义是调用 createPatchFunction 方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。其中 nodeOps 封装了一系列 DOM 操作的方法, modules 定义了一些模块的钩子函数的实现。

createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法。具体源码位置src\core\vdom\patch.js,源码过长就不放出来。

小结

了解了 Vue 的整个初始化过程,得出

  • Vue 初始化过程是怎样的(new Vue() 发生了什么)

    1. _init() 初始化数据、状态等
      • 处理组件配置项
        • 初始化组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上
        • 初始化每个子组件做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
      • 初始化组件实例的关系属性,比如 $parent、$children、$root、$refs等
      • 处理自定义事件
      • 调用 beforeCreate 钩子函数
      • 初始化组件的 inject配置,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行浅层的响应处理,并代理每个 key 到 vm 实例上
      • 数据响应式,处理 props、methods、data、computed、watch 等选项
      • 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
      • 调用 created 钩子函数
      • 如果有 el 配置,自动 $mount
      • 进入挂载阶段
  • 生命周期过程是什么
    vue init