Vue3两种写法对比

Vue 3 两种写法对比

刚开始学 Vue 的时候我在网上抄代码, 发现一个很困惑的事: 有的教程里组件是 export default { data() {...}, methods: {...} } 这种一大坨配置, 有的教程上来就 const count = ref(0), 长得完全像两个框架。
后来才明白这是 Vue 不同年代的写法。这篇笔记用同一个小组件把三种写法各写一遍, 捋清楚它们是怎么一步步演化过来的, 以后不管翻到哪个年代的代码都能认得。

读这篇之前需要会: JS 基础 (尤其 const、箭头函数)、import/export 模块语法 (工具链篇讲过)、知道 .vue 单文件组件长什么样。

例子: 一个用户卡片

下面三种写法实现的是完全同一个功能: 两个输入框绑定用户名和年龄, 显示 “21岁” 这样的计算结果, 一个按钮给年龄 +1。模板部分三种写法完全一样, 变的只是 <script> 里怎么写:

代码块VUE · 7 行收起展开
<template>
  <div>
    <input v-model="userName" />{{ userName }}<br />
    <input v-model.number="age" />{{ displayAge }}<br />
    <button @click="addAge">+1</button>
  </div>
</template>

写法一: 选项式 API, Vue 2 的经典款

Vue 2 时代 (以及很多老项目) 的组件长这样:

代码块VUE · 29 行收起展开
<script>
// export default 是 ES 模块的默认导出, 意思是"这个文件对外就交出这一个东西"
// 别的文件 import 这个 .vue 文件时, 拿到的就是这个配置对象
export default {
  // 响应式数据: 页面上会变的东西都放这里
  data() {
    return {
      userName: 'aaa',
      age: 21
    }
  },
  // 计算属性: 由其他数据算出来的值
  computed: {
    displayAge() {
      return this.age + '岁'
    }
  },
  // 方法: 事件处理函数都放这里
  methods: {
    addAge() {
      this.age += 1
    }
  },
  // 生命周期钩子: 组件挂载到页面后执行
  mounted() {
    console.log('组件挂载')
  }
}
</script>

整个组件就是一个大配置对象, datacomputedmethodswatchmounted 这些名字全是 Vue 规定死的, 你往对应的格子里填内容, Vue 负责把它们组装成一个能跑的组件。对我这种写惯 Java 的人, 感觉很像实现一个框架接口: 方法名是约定好的, 你只管填实现。

两个当年最容易懵的点:

data 为什么是个函数? 因为组件会被复用, 页面上放三个用户卡片就是三个实例。每个实例调用一次 data(), 各自拿到一份独立的数据副本。要是直接写成对象, 所有实例就共享同一份数据了, 相当于把 Java 类的成员变量写成了 static, 一个卡片改年龄三个卡片一起变。

this 是哪来的? Vue 在背后把 data 返回的数据、methods 里的方法全都挂到了组件实例上, 所以方法里用 this.age 就能摸到数据。
这个 this 用起来像 Java 对象里的 this, 但它是 Vue 施了魔法的代理对象, 这也是选项式写法里箭头函数不能用来定义 method 的原因 (箭头函数没有自己的 this)。

这套写法的问题要等组件变大才暴露: 一个功能的代码被硬拆到三个格子里。比如”搜索”这个功能, 它的关键词在 data, 过滤逻辑在 computed, 请求方法在 methods, 中间还隔着其他功能的代码。改一个功能要在文件里上下反复跳。

写法二: setup() 函数, Vue 3 刚出时的样子

Vue 3 (2020 年) 提出组合式 API 来解决”代码被格子拆散”的问题: 数据和逻辑不再按类型分格子, 全都写在一个 setup() 函数里, 想怎么组织就怎么组织。最早的写法长这样:

代码块VUE · 29 行收起展开
<script>
// ref 是从 vue 这个 npm 包里导入的函数 (node_modules/vue, 创建项目时装好的)
// 它的作用: 把一个普通值包成"响应式"的, 值一变页面就跟着变
import { ref, computed } from 'vue'

export default {
  // setup 是 Vue 3 新增的选项, 组件创建前执行一次
  setup() {
    const userName = ref('aaa')
    const age = ref(21)

    // .value 是 ref 的开箱动作: 在 JS 代码里读写 ref 必须通过 .value
    const displayAge = computed(() => age.value + '岁')

    function addAge() {
      age.value += 1
    }

    // 最坑的一步: 模板要用的东西必须在这里手动 return 出去
    // 忘了 return 的变量, 模板里拿到的就是 undefined
    return {
      userName,
      age,
      displayAge,
      addAge
    }
  }
}
</script>

进步很明显: setup 里没有 this 了, 数据、计算属性、方法可以按功能挨着写, 一个功能的代码终于能聚在一起。

但用过就知道哪里难受: 写完一堆变量和函数, 结尾还得手动 return 一大串名单。变量一多, 每加一个就得去名单里补一行, 忘一个页面就悄悄空白。纯样板劳动, 没有任何信息量。

写法三: <script setup>, 现在的默认款

Vue 3.2 (2021 年) 给上面的写法加了语法糖, 也就是现在新项目的标准写法:

<script setup>
import { ref, computed } from 'vue'

const userName = ref('aaa')
const age = ref(21)

const displayAge = computed(() => age.value + '岁')

function addAge() {
  age.value += 1
}
</script>
对比写法二, 消失的东西: `export default` 没了, `setup() {}` 这层壳没了, 手动 `return` 没了。原理是编译器帮你干了这些活: `<script setup>` 里的顶层变量和函数, 编译时自动全部暴露给模板。你写的每一行都是业务, 样板代码归零。

顺带的好处: `import` 进来的子组件不用再注册, 直接在模板里用; 配合 TypeScript 时类型推导也最完整 (TypeScript 是给 JS 加静态类型检查的方言, 工具链篇提过, 从 01 篇开始我的示例会慢慢用上它)。

三种写法对照

选项式 APIsetup() 函数<script setup>
年代Vue 2Vue 3.0 (2020)Vue 3.2 起 (2021)
数据data() { return {} }const x = ref()const x = ref()
访问数据this.xx.valuex.value
方法methods: { fn() {} }function fn() {}function fn() {}
给模板暴露自动 (挂到 this)手动 return自动 (顶层即暴露)
代码组织按类型分格子按功能聚合按功能聚合

心智模型的变化就一句话: 从”往框架规定的格子里填空”, 变成”写普通的变量和函数, 框架负责让它们变响应式”。

还有一个演进动机: 逻辑复用

选项式时代想在多个组件之间复用一段逻辑 (比如”计数”), 用的是 mixins: 把一个配置对象混进组件里。问题是混进来的东西和组件自己的东西共享命名空间, data 里重名就打架, 而且光看组件代码根本不知道 count 是哪来的。写 Java 的人应该秒懂, 这就是多继承的经典毛病。

组合式 API 用普通函数解决这件事, 约定俗成叫 composable:

// useCounter.js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  function increment() { count.value++ }
  return { count, increment }
}
// 组件里
import { useCounter } from './useCounter'

const { count, increment } = useCounter()

来源清清楚楚 (import 写在那里), 重名可以解构时改名, 每次调用各拿一份独立状态。组合优于继承这句话, 前端也一样适用。组合式函数在 02 篇还会展开: 组件设计与数据流

实际怎么选

我自己的原则: 新代码一律 <script setup>; 选项式 API 保持”能读懂”的水平就行, 因为网上大量教程、公司里的存量项目还是它; 同一个组件里不要两种混着写。真接手 Vue 2 老项目时, 这篇里写法一的知识就是用来读代码的。

常见疑问

.value 到底什么时候写? JS 代码里读写 ref 都要 .value; 模板 {{ }} 里不用, Vue 自动帮你拆箱。忘写 .value 是新手第一大坑, 01 篇会细讲 ref 的原理。

this 去哪了? 组合式 API 里没有 this。数据就是你亲手声明的那些变量, 直接用变量名访问, 反而比记”Vue 把什么挂到了 this 上”简单。

两种写法能混用吗? 技术上一个组件可以同时有 <script><script setup> 两个块, 但除了极特殊场景 (比如要写组件的 name 选项) 别这么干, 可读性很糟。

参考:

延伸阅读