new Vue() 的时候发生了什么

寻找 Vue 函数

要知道 new Vue() 的时候发生了什么,首先要知道 Vue 定义在哪里。

我们从 package.json 开始寻找 Vue 在哪里

  1. scripts 中,可以看到在 build 时执行了 scripts/build.js
  2. 找到 scripts/build.js ,发现在构建时读取了同目录下的 config.js
  3. 找到 scripts/config.js ,根据 package.json 中的 main 字段可以知道最终入口是 dist/vue.runtime.common.js ,于是我们找 dest 为 dist/vue.runtime.common.js 对应的 entry ,发现是 web/entry-runtime.js
  4. 找到 entry-runtime.js (别名在 scripts/alias.js 中),顺着 import Vue 一直找下去
  5. 最后发现,在 src/core/instance/index.js 中定义了 function Vue

import Vue 时发生了什么

平时在写 Vue 项目时,一般都是先导入 Vue ,然后再执行 new Vue()

那么,在我们 import Vue from 'vue' 的时候都发生了什么呢,或者说,new Vue() 之前框架都做了哪些准备工作

我们从 rollup 打包的入口,也就是上一步中的 web/entry-runtime.js 开始看(单页应用的话引入的可能是 vue.esm.js ,对应入口是 web/entry-runtime-with-compiler.js ,与运行时版本相比,其 $mount 是带了编译模板功能的,这个暂时不看),会发现在 web/runtime/index.js 中,主要是在 Vue.prototype 上挂了一个 $mount 方法,这个方法,就是我们执行 (new Vue()).$mount() 时调用的方法

从当前文件接着寻找 import Vue ,可以找到 core/index.js ,在这个文件中,主要是挂载 ssr 相关的属性, $isServer, $ssrContext

接着找 import Vue ,就找到了定义 Vue 的地方, core/instance/index.js 。在 function Vue 底下对 Vue 函数执行了一些混入操作:

1
2
3
4
5
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

这一堆 mixin 方法都是从同目录下的文件中导入的,我们一个个来看

initMixin

文件: src\core\instance\init.js

主要内容: 挂载 _init 方法到 Vue.prototype 中, _initnew Vue() 时首先调用的方法。

stateMixin

文件: src\core\instance\state.js

主要内容: 挂载 $data, $props 属性与 $set, $delete, $watch 方法到 Vue.prototype

eventsMixin

文件: src\core\instance\events.js

主要内容: 挂载 $on, $once, $off, $emit 方法到 Vue.prototype

lifecycleMixin

文件: src\core\instance\lifecycle.js

主要内容: 挂载 _update, $forceUpdate, $destroy 方法到 Vue.prototype

renderMixin

文件: src\core\instance\render.js

主要内容: 挂载 $nextTick, _render 方法到 Vue.prototype

到这里,准备工作就基本完成了,总结起来就是往 Vue.prototype 中挂载了一堆属性与方法

new Vue() 时发生了什么

准备工作完成后,接下来就是等待用户执行 new Vue() 了。

我们从定义 Vue 的地方,就是 src\core\instance\index.js 开始看起。

function Vue 中除了提示我们 Vue 是一个需要用 new 的构造函数外,只调用了一个方法:

1
this._init(options)

这个即是刚才 initMixin 中挂载到 Vue.prototype 上的 _init 方法。

_init 中主要做了这些事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 省略部分代码
const vm: Component = this
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 (vm.$options.el) {
vm.$mount(vm.$options.el)
}

可以看到,在生命周期 beforeCreate 之前,完成了 initLifecycle, initEventsinitRender。前两个不说,在 initRender 中,挂载了 $slots, $scopedSlots, $attrs, $listeners 属性和 $createElement 方法。

而在 created 之前,完成了 initInjections, initStateinitProvide 。这边我们主要看一下 initState ,因为这跟我们平时所写的 Vue 组件的属性关系比较大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

在这里,我们可以看到每个属性初始化的顺序:

1
props -> methods -> data -> computed -> watch

根据这个顺序就可以明白,为什么在 props 里面没法访问 methods 或 data ,在 data 里面能访问 methods 却无法访问 computed

而 props 的 validator ,因为是直接调用执行的,因此内部无法访问 Vue 实例;而且 props 是最先被初始化的,在这个步骤对于实例内的属性几乎没有什么可访问的,个人感觉即使绑定了 this 意义也不大。

接下来就是判断是否提供了 el 属性,如果有,则执行 $mount 方法挂载组件,这个过程中,会调用 src\core\instance\lifecycle.js 中的 mountComponent 。由于在这个方法中也存在生命周期钩子函数的调用,下面大概来看一下 mountComponent

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
// 删除了非 production 时的代码
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')

let updateComponent
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
}

从上面的代码可以看到, beforeMount 之前会先判断是否有 render 属性,如果没有则赋值一个空的 VNode 节点。这里的 render 其实是一定会有的:

  1. 如果你写的是单文件组件,那 <template></template> 会被 vue-loader 编译为 render 函数
  2. 如果你提供的是 template 选项,并使用带 compiler 版本的 Vue ,则在 $mount 方法中会进行编译成 render 函数
  3. 如果直接写的 render 函数,有 jsx 则会编译成 JavaScript ,这在 Vue 官方文档中是有说明的

然后是 new 了一个 Watcher 。这里目前不需要知道里面具体干了什么,只要知道 updateComponent 会马上被调用一次就行了。而 updateComponent 中调用了 _render_update 方法,之前已经看过,这两个方法分别在 render.jslifecycle.js 中挂载到 Vue 的原型上了。简单来说,这一步创建了当前实例的 VNode 。

至此, new Vue() 表面可见的工作大致就到这边,剩下的 watch 相关、 vdom 以及其他内容后续再看。