Vue钩子函数中的this为什么能指向Vue的实例而不是指向传入的参数options(Vue源码解读)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/milugloomy/article/details/100703831

起因

先看一段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的实例。

猜你喜欢

转载自blog.csdn.net/milugloomy/article/details/100703831
今日推荐