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 以及其他内容后续再看。