new Vue() 的时候发生了什么
寻找 Vue 函数
要知道 new Vue() 的时候发生了什么,首先要知道 Vue 定义在哪里。
我们从 package.json
开始寻找 Vue 在哪里
- 在
scripts
中,可以看到在 build 时执行了scripts/build.js
- 找到
scripts/build.js
,发现在构建时读取了同目录下的config.js
- 找到
scripts/config.js
,根据package.json
中的main
字段可以知道最终入口是dist/vue.runtime.common.js
,于是我们找 dest 为dist/vue.runtime.common.js
对应的 entry ,发现是web/entry-runtime.js
- 找到
entry-runtime.js
(别名在scripts/alias.js
中),顺着import Vue
一直找下去 - 最后发现,在
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 | initMixin(Vue) |
这一堆 mixin 方法都是从同目录下的文件中导入的,我们一个个来看
initMixin
文件: src\core\instance\init.js
主要内容: 挂载 _init
方法到 Vue.prototype
中, _init
是 new 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 | // 省略部分代码 |
可以看到,在生命周期 beforeCreate
之前,完成了 initLifecycle
, initEvents
和 initRender
。前两个不说,在 initRender
中,挂载了 $slots
, $scopedSlots
, $attrs
, $listeners
属性和 $createElement
方法。
而在 created
之前,完成了 initInjections
, initState
和 initProvide
。这边我们主要看一下 initState
,因为这跟我们平时所写的 Vue 组件的属性关系比较大。
1 | export function initState (vm: Component) { |
在这里,我们可以看到每个属性初始化的顺序:
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 | // 删除了非 production 时的代码 |
从上面的代码可以看到, beforeMount
之前会先判断是否有 render
属性,如果没有则赋值一个空的 VNode 节点。这里的 render
其实是一定会有的:
- 如果你写的是单文件组件,那
<template></template>
会被vue-loader
编译为render
函数 - 如果你提供的是
template
选项,并使用带 compiler 版本的 Vue ,则在 $mount 方法中会进行编译成render
函数 - 如果直接写的
render
函数,有 jsx 则会编译成 JavaScript ,这在 Vue 官方文档中是有说明的
然后是 new
了一个 Watcher
。这里目前不需要知道里面具体干了什么,只要知道 updateComponent
会马上被调用一次就行了。而 updateComponent
中调用了 _render
跟 _update
方法,之前已经看过,这两个方法分别在 render.js
跟 lifecycle.js
中挂载到 Vue 的原型上了。简单来说,这一步创建了当前实例的 VNode 。
至此, new Vue()
表面可见的工作大致就到这边,剩下的 watch 相关、 vdom 以及其他内容后续再看。