为什么 Vue 2 的 this 能够直接获取到 data 和 methods?
原文讨论的是 Vue 2 Options API。本文保留源码探究主线,并补充 Vue 2.7 与 Vue 3.5 的边界。
先纠正一个前提:普通 JavaScript 函数的
this不是天然指向methods对象。Vue 2 在初始化时主动把 method 绑定到组件实例,所以方法内部的this才会指向 vm。
一、先看使用现象
// Vue 2.7,需要 Vue 2 构造器和浏览器 DOM
new Vue({
el: '#app',
data: {
message: 'Deno',
},
methods: {
showMessage() {
console.log(this.message)
},
},
})
<div id="app">
<button @click="showMessage">显示消息</button>
</div>
点击按钮时,showMessage 内的 this 是 Vue 组件实例 vm,不是 methods 对象。this.message 之所以存在,是因为 Vue 把 data 代理到了 vm 上。


二、Vue 2 的 data 代理
Vue 2 初始化 data 时,会把 data 解析成 vm._data,大致流程是:
function initData(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
Object.keys(data).forEach(key => {
proxy(vm, '_data', key)
})
observe(data)
}
源码中的 proxy 可以简化为:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => undefined,
set: () => undefined,
}
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(value) {
this[sourceKey][key] = value
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
因此:
vm.message
↓
vm._data.message
↓
Vue 2 响应式 getter/setter
代理不是复制数据。vm.message 和 vm._data.message 访问的是同一份状态,只是增加了一层转发。

2.1 为什么 data 通常写成函数
组件可能被创建多个实例。如果把同一个对象作为所有实例的 data,就会导致实例之间共享状态:
// Vue 2 组件:推荐
export default {
data() {
return {
count: 0,
}
},
}
Vue 2 的根实例可以使用对象形式,但组件的 data 必须是函数。写成函数可以让每次创建组件实例时得到新的对象。
三、Vue 2 的 methods 初始化
Vue 2 的 initMethods 会遍历 methods,把函数绑定到 vm:
function initMethods(vm, methods) {
const props = vm.$options.props
for (const key in methods) {
if (typeof methods[key] !== 'function') {
vm[key] = function noop() {}
} else {
vm[key] = methods[key].bind(vm)
}
}
}
真实源码还会检查方法名是否与 props 冲突,并在开发环境发出警告。
3.1 bind 到底做了什么
const methods = {
showMessage(prefix) {
console.log(prefix, this.message)
},
}
const vm = {
message: 'Vue',
}
vm.showMessage = methods.showMessage.bind(vm)
vm.showMessage('message:')
bind(vm) 固定的是函数调用时的 this,并不会把 vm 当成用户函数的第一个业务参数:
function show(prefix) {
console.log(this.message, prefix)
}
const bound = show.bind({ message: 'Vue' })
bound('hello') // this.message 是 Vue,prefix 是 hello
当用户写 @click="showMessage" 时,Vue 事件系统调用的已经是绑定过的函数,所以无论它被作为回调传给哪里,this 都能保持为组件实例。Vue 官方也提醒,不要用箭头函数定义需要使用组件 this 的 methods,因为箭头函数没有自己的动态 this:
export default {
methods: {
// 正确
show() {
console.log(this.message)
},
// 不推荐:箭头函数不会获得 Vue 实例 this
wrong: () => {
// this 不是组件实例
},
},
}
四、为什么 data 和 methods 采用不同处理方式
data
data 需要通过 accessor 把 vm.key 转发到 _data.key,这样读取和赋值才能进入 Vue 2 的响应式 getter/setter:
Object.defineProperty(vm, 'message', {
get() {
return vm._data.message
},
set(value) {
vm._data.message = value
},
})
methods
methods 的主要工作是把函数绑定到正确的实例,然后作为 vm 的方法暴露。使用直接赋值更简单:
vm.show = methods.show.bind(vm)
这不是因为 methods “不能使用 Object.defineProperty”,也没有足够依据说直接赋值一定是性能原因。使用 accessor 当然也能实现,只是 Vue 2 的 API 设计选择了绑定后直接赋值。
五、Vue 2 响应式与 this 的关系
vm.message 的代理只解决了访问路径,真正的响应式来自 _data 上的 getter/setter:
读取 vm.message
↓
proxy getter
↓
读取 vm._data.message
↓
defineReactive getter 收集当前 watcher
设置 vm.message = next
↓
proxy setter
↓
设置 vm._data.message
↓
defineReactive setter 通知 watcher
Vue 2.7 还会通过 scheduler 对渲染 watcher 去重并异步刷新,因此 setter 执行并不意味着 DOM 已经同步更新。需要读取更新后的 DOM 时,应使用 this.$nextTick()。

六、Vue 3.5 还可以这样理解吗
Vue 3 仍然支持 Options API:
<script>
export default {
data() {
return {
message: 'Vue 3',
}
},
methods: {
showMessage() {
console.log(this.message)
},
},
}
</script>
但底层结构已经不同。Vue 3 的组件实例有一个 public instance proxy,访问 this.message 时会按照 setup state、data、props、ctx 和公共属性等来源查找:
this.message
↓
组件 public instance Proxy
↓
setupState / data / props / ctx / publicPropertiesMap
Vue 3 Options API 的 methods 也会绑定到 public proxy,但不能再把 Vue 2 的 vm._data、proxy(vm, '_data', key) 当成 Vue 3 的源码解释。
6.1 Composition API 没有 Options API 的 this
setup() 和 <script setup> 中推荐直接使用 props、ref、reactive 和闭包函数:
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Vue 3')
function showMessage() {
console.log(message.value)
}
</script>
<template>
<button @click="showMessage">{{ message }}</button>
</template>
这里没有 this.message。模板会自动解包顶层 ref,JavaScript 代码中仍需要写 message.value。
Vue 2.7 回移了部分 Composition API,但底层仍是 Vue 2 的 getter/setter 响应式,不能因为 API 形式相似就推断它具有 Vue 3 Proxy 的所有能力。
七、常见误区
误区 1:this 天然指向 methods
错误。普通函数的 this 由调用方式决定。Vue 2 在初始化阶段调用 bind(vm),才让 methods 中的 this 指向实例。
误区 2:bind 把 vm 作为第一个参数
错误。bind(vm) 固定的是 this;业务参数仍从调用处开始计算。
误区 3:每个 .vue 文件就是一个实例
错误。.vue 文件是组件定义,同一组件可以创建多个实例,每个实例有自己的 data 和 public proxy。
误区 4:Vue 3 setup 里继续使用 this
不推荐。Composition API 应使用显式的变量、props、emit 和 composable,不要把 Options API 的访问模型混入 setup。
误区 5:所有 data 都可以直接新增属性
Vue 2 对象新增属性、数组下标和 length 修改存在响应式限制,需要 this.$set/Vue.set 或 splice;Vue 3 Proxy 改善了这些限制,但 Vue 2 的旧项目仍需遵守旧规则。
八、总结
- Vue 2 把 data 保存到
vm._data,再用Object.defineProperty代理到 vm; - Vue 2 把 methods 用
bind(vm)绑定后直接挂到实例; - methods 中的
this指向 vm 是 Vue 初始化的结果,不是 JavaScript 的默认行为; - data 通常写函数是为了让每个组件实例获得独立对象;
- Vue 3 Options API 仍有
this,但底层是 public instance Proxy; - Vue 3 Composition API 使用 ref/reactive,不依赖
this; - 文章中的源码是版本实现细节,实际项目应以对应 Vue 版本源码为准。