起因
先看一段Vue的代码,在Vue的原型链上增加了一个setData方法,然后实例化Vue对象,传入一个Object类型的参数
Vue.prototype.setData = function (key, val) {
if (this.data) {
this.data[key] = val
} else {
this.data = {
[key]: val
}
}
}
let options = {
el: '#divBody',
created: function () {
this.setData('a', 1)//①
},
mounted () {
console.log(this.data.a) //②
}
}
new Vue(options)
这段代码在②处能正确输出1。也就是说我们在Vue原型链上增加的任何函数,在created、mounted、methods等Vue的生命周期函数中的this都能获取到。
再来来看下面一段代码,这里用MyVue函数模拟Vue函数,然后在Page函数的原型链上也增加setData方法。
function MyVue(obj) {
this.data = {}
obj.created()
obj.mounted()
}
MyVue.prototype.setData = function (key, val) {
this.data = {
[key]: val
}
}
let options = {
created() {
this.setData('a', 1) //①
},
mounted() {
console.log(this.data.a)
}
}
new MyVue(options)
这段代码在运行时会在①处抛出一个错误,错误为:Uncaught TypeError: this.setData is not a function。
是不是和我们认知的初始化Vue对象不一样,笔者被这个问题困扰了好几天,为什么自己模拟的就会报错,网上也寻找不到答案,只有自己阅读源码来查找原因了。
分析
先自己分析下,既然错误为:Uncaught TypeError: this.setData is not a function,那肯定是this的指向出了问题。关于this的原理,可以查看这一篇《JavaScript 的 this 原理》。
了解了JavaScript的this原理后,再来看看笔者用来模拟Vue的那段代码中,①处的this指向的不是MyVue的实例,指向的是options。我们是在MyVue的原型链上增加的setData方法,又没有在options的原型链上增加setData方法。那①处的this当然会报错了。
那Vue是怎么做到在各个钩子函数中能使用this调用Vue原型链上的函数的呢?
在了解了JavaScript的this原理后,笔者分析有如下三种方案。
1、将Vue原型链上的所有函数赋给options,示例代码如下:
function Vue(options) {
Object.assign(options, Vue.prototype)
//...
}
2、将options的原型链指向Vue函数的原型对象,示例代码如下:
function Vue(options) {
options.__proto__ = Vue.prototype
//...
}
3、在Vue各个钩子函数执行时,通过call方法修改函数运行时this的指向:
function Vue(options) {
//...
options.created.call(this)
options.mounted.call(this)
//...
}
将这三种方式在模拟的MyVue构造函数中实现后,①处的this.setData都能正常执行。
那么Vue使用的是那种方式呢,下面我们来看Vue源码。
解读
笔者看的Vue源码是这个https://cn.vuejs.org/js/vue.js。
首先Vue实例化的时候会执行mergeOptions函数,如下:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这段代码的意思是把构造函数上的options和实例化时传入的options进行合并操作并生成一个新的options,并赋值给vm.$options
在Vue实例化,也就是new Vue(options)时,还会执行以下初始化的函数
vm._self = vm
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')
我想你们应该看到了callHook(vm, 'beforeCreate')和callHook(vm, 'created')这两个函数。没错,这两个函数就是Vue的生命周期中的两个钩子函数。也就是说在Vue实例化时,会执行Vue声明中期中的beforeCreate和created这两个钩子函数。
接着看callHook函数,callHook函数会调用invokeWithErrorHandling函数,我们再来看看invokeWithErrorHandling这个函数。
function invokeWithErrorHandling (handler,context,vm,info) {
var res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) {
return handleError(e, vm, info + ' (Promise/async)')
})
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
关键代码:
res = args ? handler.apply(context, args) : handler.call(context)
这里的context是vm,就是Vue的实例,也就是说Vue是在函数执行的时候使用call和apply改变了函数this的指向,采用的是以上描述的第三种方式。
结论
Vue钩子函数中的this为什么能指向Vue的实例而不是指向传入的参数options,是因为Vue在钩子函数执行时使用call和apply更改了this的指向,使得我们在Vue的各个钩子函数,created,mounted等函数中取到的this指向Vue的实例。