二 · Let there be Vue

每一个学习Vue的新手,一定写过这样的一句代码:

1
2
3
4
5
6
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})

这个出自Vue官方文档的Hello World代码里,上手就教了你new Vue(),但这个Vue从何而来,又为什么必须要new出来呢?这次我们就试着弄清楚这个问题。

在翻开Vue源码之前,我们可以试着在引入Vue的页面里写上面那段代码,只是拿掉了new:

1
2
3
4
5
6
var app = Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})

这样的话,浏览器会提示你:Cannot read property ‘_init’ of undefined,如果你引的是非生产版本,还可能会提示你:Vue is a constructor and should be called with the ‘new’ keyword.

new的用法和含义可以参看MDN文档,不过我们不妨先来看看Vue的诞生地,这段代码位于core/instance/index.js里,这里清楚地表明了Vue的诞生过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'

function Vue (options) {
// 省略调试相关代码
this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

前文提过的_init其实就在这个地方。Vue本身是一个构造函数,如果我们不把它当成一个构造函数使用,那么我们调用Vue会执行this._init方法,在严格模式下(如果有看过最终打包出来的Vue代码,会在开头找到’use strict’)直接调用的话,this的指向是undefined,所以会抛出Cannot read property ‘_init’ of undefined的异常。

那为什么我们使用new就不会报错了呢?

一般而言,在其他的面向对象的语言里(例如Java),new的作用是创建一个对象的实例,但JavaScript这门语言是基于原型的,与一般意义的面向对象的语言不太一样(其中具体的区别可以参考MDN文档)。当我们使用new Vue()的时候,JavaScript帮我们完成了一些事情:

  1. 生成一个临时对象
  2. 将该临时对象的原型链指向目标对象
  3. 返回该对象

也就是说,new Vue()这句代码可以约等于以下这段代码:

1
2
3
let newVue = {}
newVue.__proto__ = Vue.prototype
Vue.call(newVue)

关于call函数的作用可以参考MDN文档,简单而言,call函数的作用就是改变函数内this的指向,将其指向一个新的目标。使用new语法可以让代码更加简洁和明白,不过也因此遭来了语法糖的嫌疑。new到底是不是语法糖,以及函数是不是JS的一等公民(这部分大家可以参考这篇文章),并不是我们这里要讨论的,重点在于后面。

一个Vue实例就这样被创建出来了,由此我们完全能够理解为什么在实际开发里,能够在Vue组件内使用this访问到Vue实例本身(比如大家都很熟悉的this.$data)。但这里还有个事情没有弄清楚:new关键字能继承原型,可是Vue构造函数并没有涉及到原型的部分啊?Vue是一个空对象,其原型似乎没有什么特别的,那我们继承个啥,继承个寂寞啊。

我们来看看Vue构造函数里执行的那个函数里。对,也就是:

1
this._init(options)

当我们new一个对象时,在临时对象最终返回前,构造函数内的所有步骤都会被执行,也就包括了这个_init函数。然而问题又来了,这个函数在此处是没有定义的,那定义在哪里了呢?

玄机就在Vue函数外面的那几个函数里,也就是:

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

initMixin函数由同级目录下的init.js文件引入,点进去一看,赫然写着:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 省略部分代码
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 省略部分代码
}
}

initMixin函数接收一个Vue对象,其作用只有一个,就是给这个Vue对象的原型里添加一个函数叫做_init!再仔细瞧瞧这个函数里的东西,好像看见了一些眼熟的什么beforeCreate、created……没错,这不就是Vue的生命周期嘛!我们也可以大胆猜想,Vue的生命周期函数就是在这些位置定义的。

换言之,在我们引入Vue之后,使用new Vue()之前,其实已经有些事情在做了,这些initMixin、stateMixin之类的函数,其作用就是对Vue的原型进行填充和完善,使我们最终使用new Vue()的时候,能够生成一个正确的实例出来,当然这只是非常粗略的说法,具体的实现细节会在之后慢慢揭开。

1
2
3
let thereBe = new Vue({
thankYou: 'for your reading!'
})