技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

为什么 Vue 2 的 this 能够直接获取到 data 和 methods?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/59-为什么 Vue2 this 能够直接获取到 data 和 methods?
本文目录9 个章节
  1. 一、先看使用现象
  2. 二、Vue 2 的 data 代理
  3. 三、Vue 2 的 methods 初始化
  4. 四、为什么 data 和 methods 采用不同处理方式
  5. 五、Vue 2 响应式与 this 的关系
  6. 六、Vue 3.5 还可以这样理解吗
  7. 七、常见误区
  8. 八、总结
  9. 参考资料

为什么 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 methods 中 this 指向实例的调试截图(历史资料)

Vue 2 实例中的 data 调试截图(历史资料)

二、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.messagevm._data.message 访问的是同一份状态,只是增加了一层转发。

Vue 2 proxy 函数源码调试截图(历史资料)

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 2 data 和 methods 初始化源码截图(历史资料)

六、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._dataproxy(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.setsplice;Vue 3 Proxy 改善了这些限制,但 Vue 2 的旧项目仍需遵守旧规则。

八、总结

  1. Vue 2 把 data 保存到 vm._data,再用 Object.defineProperty 代理到 vm;
  2. Vue 2 把 methods 用 bind(vm) 绑定后直接挂到实例;
  3. methods 中的 this 指向 vm 是 Vue 初始化的结果,不是 JavaScript 的默认行为;
  4. data 通常写函数是为了让每个组件实例获得独立对象;
  5. Vue 3 Options API 仍有 this,但底层是 public instance Proxy;
  6. Vue 3 Composition API 使用 ref/reactive,不依赖 this
  7. 文章中的源码是版本实现细节,实际项目应以对应 Vue 版本源码为准。

参考资料

原文出处:为什么 Vue2 this 能够直接获取到 data 和 methods?

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS