【若川视野 x 源码共读】第23期 | Vue2 this 能够直接获取到 data 和 methods

962 阅读2分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

前言

本文将就 vue2 中 this 能够访问到 data、methods及props中定义的数据展开。主要涉及 vue2 的源码。

阅读本文,你将学到:

1. 如何调试vue2源码
2. vue2 有关 data、methods、props 初始化过程
3. 由阅读源码的收获和实用操作
4. 等等

准备工作

由于是要调试vue2源码,那么自然需要一个开发环境能够直接对vue2源码进行调试。这里我采用的是方式是基于 webpack搭建开发环境,同步编译 vue2 源码的方式。具体步骤如下:

  • 通过webpack搭建一个开发环境位于 app目录下
  • git clone 一份 vue2 的源码至 vue 目录下
  • 修改 webpack 配置,增加 alias 配置,指定 vuejs 路径为 vue2 源码构建的产物
  • 安装 app、vue 的依赖,分别启动app的devServer和 vue 的编译监听模式

安装依赖

cd ./app & npm install 
cd ./vue & npm install

运行调试

cd ./app & npm start 
cd ./vue & npm run dev
  • 在vscode中调试vue2,参考cn.vuejs.org/v2/cookbook…。这样就可以直接通过f5打开调试窗口,并且直接通过vscode进行断点调试。

附上代码:📎vue-analysis.zip

vue2 有关 data、methods、props 初始化

查阅 vue/src/core/instance/init.js 可以看到初始化阶段主要执行了以下逻辑 初始化生命周期 -> 初始化事件中心 -> 初始化渲染 -> 执行 ‘beforeCreate’ 生命周期 -> 初始化inject -> 选项初始化;初始化data、props、computed、watcher... -> 初始化provide -> 执行 'created' 生命周期

所以有关 data、methods、props 初始化阶段是位于 initState 。它在 beforeCreate 之后,created 之前。所以这也是为什么在 beforeCreate 中无法通过 this 获取到 data、props、methods。

注意,这里说的是无法通过 this.xxx 的方式直接获取到。但是如果你要问我能不能获取到,我的答案是可以的。

在初始化阶段,有一段 merge options 的操作,这会导致能够通过 this.options获取到定义的data函数。从而可以通过this.options 获取到定义的 data 函数。从而可以通过 this.options.data().xxx 获取到 data 中定义的数据。但是注意,这里的数据只是通过 data 函数创建的副本,和 vue 实例对象上的 data 没有关联。所以并没有太多的实际意义。这里提到 this.$options.data() 的使用,是否能够联想到一些使用操作呢?

接着来看以下 initState 中都做了哪些事情,查阅源码 vue/src/core/instance/state.js:

  • 【52】初始化 props
  • 【53】初始化 methods
  • 【54-58】初始化 data
  • 【59】初始化 computed
  • 【60-62】初始化 watch

可以看到选项初始化阶段先后顺序,这里记一下先后顺序,后续有由于初始化顺序产生的一些问题。

initProps

initProps主要实现:

遍历定义的 props 配置。遍历的过程主要做两件事情:

1)调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性

2)通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上

这也就是为什么能够通过 this.xxx 访问到 props 中的数据

initMethods

methods 初始化阶段相对比较简单,因为不需要进行数据响应式,只是简单的对 methods 选项进行遍历,直接通过 vm[key] = ...【289】的方式进行赋值操作。

initData

initData 函数主要完成如下工作:

  • 根据 vm.options.data选项获取真正想要的数据(注意:此时vm.options.data 选项获取真正想要的数据(注意:此时 vm.options.data 是函数)
  • 校验得到的数据是否是一个纯对象
  • 检查数据对象 data 上的键是否与 props 对象上的键冲突
  • 检查 methods 对象上的键是否与 data 对象上的键冲突
  • 在 Vue 实例对象上添加代理访问数据对象的同名属性
  • 最后调用 observe 函数开启响应式之路

遍历定义的 data 配置。遍历的过程主要做两件事情:
1)对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;
2)调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性

proxy

可以看到 initData 和 initProps 中,都是最终调用 proxy 来实现的。注意这里并不是 es6 中的 Proxy,而是内置的 proxy 函数。下面看一下 proxy 的具体实现

实际上核心是通过 Object.defineProperty 把 target[sourceKey][key] 的读写变成了对 target[key] 的读写。

至此在 vue2 中能够通过 this 访问到 data 和 props 的全过程已解析完毕 。

总结

Vue中为什么可以通过 this.xxx 访问到 data 或者 props 中的值 ?

是在初始化阶段完成的,具体实现是在 _init 中 initState(vm) 对选项初始化 (vue/src/core/instance/state.js) ,

其中有 initData(vm) 对 data 选项进行初始化,最终通过 proxy 实现代理。

同理对于 props,也是同样的实现。其核心是在对 data,props 选项初始化阶段通过 proxy 实现代理。其核心是通过 Object.defineProperty 实现代理。

那么对于通过 this.xxx 能够访问 methods 选项上定义的访问是怎么实现的呢?也是通过 proxy 实现代理的吗?

同样的是,它们都发生在选项初始化的阶段。但实现的步骤和 data,props 有所不同,并不是通过 proxy 实现的。

具体步骤是在 initMethods(vm, opts.methods) (vue/src/core/instance/state.js)中:

循环 methods 选项并直接挂载在 vue实例对象上,

vm[key] = methods[key] == null ? noop : bind(methods[key], vm)

为什么在vue data(){} 函数中能够通过 this.xxx 访问到 props 传入的数据?

这个问题核心主要有两个:

第一点是:在Vue初始化阶段,初始化 data、props的过程是 props 先于 data(vue\src\core\instance\state.js)。如果改变一下初始化顺序,试一下则可知,在data中访问不到props上传值了。

第二点是:在 initData 中,通过 getData 获取数据对象,并指定this指向vue实例对象。

关于 props, data, methods 选项的中定义相同属性,优先级是如何,为什么?

首先优先级是: props > data > methods

原因是:在 initState 中可以发现: initProps, initMethods, initData 先后执行。

在 initProps 中并没有进行判断选项是否存在于 data 或者 methods 中;

在 initMethods 中判断了 props 中是否有相同的选项;

在 initData 中判断了 methods , props 上是否有相同的选项;

因而优先级关系是 props优先级 > data优先级 > methods优先级。

即如果一个 key 在 props 中有定义了那么就不能在 data 和 methods 中出现了;如果一个 key 在 data 中出现了那么就不能在 methods 中出现了

以上只是官方的解释。其希望的是在使用的使用遵循以上的优先级。因为其中很多只是在非生产环境的报错提示,而不是真正影响代码的运行。

因而,在实际使用中,可以分为两种情况,一种是有 methods 中定义相同 key 的情况,另一种是没有:

1)props > data

先执行 initProps,再执行 initData 。而在 initData 中,会判断是否在 props 中存在相同的 key,如果有则不代理到实例上(proxy(vm, _data, key))。

2)data > methods

先执行了 initMethods,再执行 initData。而在 initData 只是做了 warn 警告处理,实际上还是会执行 proxy(vm, _data, key)。

3)methods > props

先执行了 initProps,再执行 initMethods。同样也只是 warn 警告,不影响之后的代码运行。最终还是会调用 vm[key] = methods[key] == null ? noop : bind(methods[key], vm);

4)methods > props,data

在三者都存在的情况下,会访问到 methods。initProps, initMethods, initData 先后执行。

前两步同第三种情况。再执行 initData,其中会因为 props 中已经定义相同的 key 而不会进行代理。所以最终会访问到 methods 中定义的 key。

在实际使用中,还是避免命名冲突的问题。尽量遵循官方的推荐思想

实用操作

重置数据

是否经常有这么个场景,有一个表单,有一个重置按钮来重置数据的。类似于:

有些组件库提供了工具方法,那么如果没有的话,该怎么做呢。给每个字段重新赋值么,如果字段很多是不是很麻烦呢。

这里可以通过 this.$options.data() 获取初始数据,进行数据重置。

<script>
  export default {
data() {
  return {
    // 表单
    form: {
      val1: '',
      val2: '',
      val3: ''
    }
  }
},
  ...
  methods: {
    // 重置表单方法
    retset() {
      this.form = this.$options.data().form;
    }
  },
    ...
}
</script>

不要在data、props、methods中定义同名字段

前面有说到,当定义到同名字段的时候,代码最终真实执行情况。但是在实际开发中,尽量避免这样的情况,因为可能导致一些不易察觉的错误

$options

可以通过 this.$options 获取定义的选项配置