Skip to content

Vue 2 Options API

大白话解释: Options API 就像"按类型分文件柜"。把组件的逻辑按类型分开存放:

  • data:数据放在这个柜子里
  • methods:方法放在这个柜子里
  • computed:计算属性放在这个柜子里
  • watch:监听器放在这个柜子里

为什么用 Options API?

  • 结构清晰:每个选项有明确的职责,新手容易理解
  • Vue 2 默认方式:Vue 2 项目都用这种方式
  • 适合简单组件:逻辑不复杂时,按类型组织很直观

Options API vs Composition API:

  • Options API:按类型组织(data、methods、computed 分开)
  • Composition API:按功能组织(同一个功能的数据、方法、计算属性放一起)

Vue 2 以选项式 API 为核心,通过 data、methods、computed、watch 等选项组织组件逻辑。这是 Vue 2 最基础也是最核心的编程范式。


组件选项完整结构

js
export default {
  name: 'MyComponent',        // 组件名,用于调试和递归组件
  components: {},              // 局部注册子组件
  directives: {},              // 局部注册自定义指令
  filters: {},                 // 局部注册过滤器(Vue 3 已移除)
  mixins: [],                  // 混入
  extends: {},                 // 扩展另一个组件
  inheritAttrs: false,         // 控制 attrs 是否应用到根元素
  model: { prop: 'checked', event: 'change' }, // 自定义 v-model

  data() { return {} },    // 组件的响应式数据,必须是函数
  props: {},              // 父组件传入的数据(只读)
  computed: {},           // 计算属性,有缓存,依赖变化时才重新计算
  watch: {},              // 侦听器,数据变化时执行副作用(发请求、操作 DOM)
  methods: {},            // 组件方法,绑定到实例上,模板中通过 @事件 调用

  beforeCreate() {},   // 实例初始化后,data/methods/computed/watch 均未初始化
  created() {},        // data/methods/computed/watch 已初始化,DOM 尚未挂载
  beforeMount() {},    // 模板编译完成,即将挂载 DOM
  mounted() {},        // DOM 已挂载,可操作 this.$el 和 this.$refs
  beforeUpdate() {},   // 数据变化后,DOM 更新前
  updated() {},        // DOM 已更新完成
  beforeDestroy() {},  // 实例仍可用,常用于清理定时器、取消监听
  destroyed() {},      // 实例已销毁,子组件和事件监听已解绑
  activated() {},      // keep-alive 缓存组件激活时调用
  deactivated() {},    // keep-alive 缓存组件失活时调用
  errorCaptured() {},  // 捕获后代组件错误时调用
}

data

组件的响应式数据,必须是函数,返回一个新的对象实例。

js
export default {
  data() {
    return {
      count: 0,
      user: { name: '张三', age: 25 },
      list: [],
      form: {
        username: '',
        password: '',
        remember: false,
      },
    }
  },
}

为什么 data 必须是函数?

js
// ❌ 错误写法 —— 对象引用共享,多个组件实例会互相影响
data: {
  count: 0,
}

// ✅ 正确写法 —— 函数返回独立对象
data() {
  return { count: 0 }
}

data 中必须声明所有响应式属性

js
data() {
  return {
    // ✅ 提前声明,后续修改是响应式的
    user: { name: '', age: 0 },
    list: [],
  }
}

// 后续赋值
this.user.name = '张三'  // ✅ 响应式
this.list.push(1)        // ✅ 响应式

⚠️ 如果 data 中没有声明某个属性,后续 this.xxx = value 不是响应式的,需要用 this.$set()。详见 Vue 2 响应式原理


props

父组件传递给子组件的数据。

基础用法

vue
<!-- 父组件 -->
<ChildComponent title="标题" :count="num" :user="userInfo" />
js
// 子组件
export default {
  // 数组写法(简单场景)
  props: ['title', 'count', 'user'],
}

对象写法(推荐,带类型校验)

js
export default {
  props: {
    // 基础类型校验
    title: {
      type: String,
      required: true,
    },

    // 多种类型
    id: {
      type: [String, Number],
      required: true,
    },

    // 带默认值
    count: {
      type: Number,
      default: 0,
    },

    // 对象/数组默认值必须用工厂函数
    user: {
      type: Object,
      default() {
        return { name: '', age: 0 }
      },
    },

    // 自定义校验函数
    status: {
      type: String,
      validator(value) {
        return ['loading', 'success', 'error'].includes(value)
      },
      default: 'loading',
    },

    // 布尔类型,默认 false
    disabled: {
      type: Boolean,
      default: false,
    },
  },
}

prop 的单向数据流

js
export default {
  props: ['initialValue'],
  data() {
    return {
      // ✅ 用 prop 初始化本地数据,后续修改不影响父组件
      currentValue: this.initialValue,
    }
  },
  watch: {
    // 监听 prop 变化,同步到本地数据
    initialValue(newVal) {
      this.currentValue = newVal
    },
  },
}

⚠️ 不要直接修改 prop。直接修改会报 warning,且父组件数据不会更新。

inheritAttrs 与 $attrs

vue
<!-- 父组件 -->
<ChildComponent type="text" placeholder="请输入" class="input" />
js
// 子组件
export default {
  // 不自动把非 prop 的 attribute 应用到根元素
  inheritAttrs: false,
  props: ['type'],

  mounted() {
    // $attrs 包含所有非 prop 的 attribute
    console.log(this.$attrs) // { placeholder: '请输入' }(class 和 style 不在 $attrs 中,自动合并到根元素)
  },
}
vue
<template>
  <!-- 手动绑定 $attrs 到指定元素 -->
  <div class="wrapper">
    <input v-bind="$attrs" />
  </div>
</template>

computed

计算属性,具有缓存特性,只有依赖变化时才重新计算。

js
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三',
      price: 100,
      quantity: 3,
      items: [
        { name: '苹果', price: 5, count: 2 },
        { name: '香蕉', price: 3, count: 5 },
      ],
    }
  },

  computed: {
    // 只读计算属性
    fullName() {
      return this.firstName + this.lastName
    },

    // 计算总价
    totalPrice() {
      return this.price * this.quantity
    },

    // 数组过滤
    expensiveItems() {
      return this.items.filter((item) => item.price > 4)
    },

    // 数组求和
    totalAmount() {
      return this.items.reduce((sum, item) => sum + item.price * item.count, 0)
    },

    // 可读写的计算属性
    fullNameRW: {
      get() {
        return this.firstName + this.lastName
      },
      set(val) {
        this.firstName = val[0]
        this.lastName = val.slice(1)
      },
    },
  },
}

computed vs methods

js
computed: {
  // ✅ computed —— 有缓存,依赖不变时不会重新计算
  expensiveCalc() {
    return this.bigList.filter(...).map(...).reduce(...)
  },
},
methods: {
  // ❌ methods —— 每次渲染都重新执行
  expensiveCalc() {
    return this.bigList.filter(...).map(...).reduce(...)
  },
}

💡 模板中 调用 computed 只在依赖变化时执行;同名 methods 每次渲染都执行。如果计算开销大,用 computed。

computed 缓存的副作用

js
computed: {
  // ❌ 不要在 computed 中执行副作用
  fullName() {
    this.sideEffectCount++ // 副作用不可控,缓存机制导致执行时机不确定
    return this.firstName + this.lastName
  },
}

methods

组件方法,绑定到组件实例上,模板中通过 @事件="方法名" 调用。

js
export default {
  data() {
    return {
      count: 0,
      inputVal: '',
      list: [],
    }
  },
  methods: {
    // 普通方法
    increment() {
      this.count++
    },

    // 带参数
    addToCount(n) {
      this.count += n
    },

    // 事件对象
    handleClick(event) {
      console.log(event.target)
    },

    // 带参数 + 事件对象
    handleClickWithArgs(msg, event) {
      console.log(msg, event.target)
    },

    // 异步方法
    async fetchList() {
      try {
        const res = await this.$http.get('/api/list')
        this.list = res.data
      } catch (err) {
        this.$message.error('请求失败')
      }
    },

    // 防抖搜索
    onSearch() {
      clearTimeout(this._searchTimer)
      this._searchTimer = setTimeout(() => {
        this.doSearch(this.inputVal)
      }, 300)
    },
    doSearch(keyword) {
      // 实际搜索逻辑
    },
  },
}
vue
<template>
  <!-- 基本调用 -->
  <button @click="increment">+1</button>

  <!-- 带参数 -->
  <button @click="addToCount(5)">+5</button>

  <!-- 带参数 + 原生事件对象 -->
  <button @click="handleClickWithArgs('hello', $event)">点击</button>

  <!-- 事件修饰符 -->
  <form @submit.prevent="onSubmit">...</form>
  <div @click.stop="handleClick">...</div>
  <input @keyup.enter="onEnter" />
</template>

⚠️ methods 中的 this 指向组件实例,不能用箭头函数定义 methods,否则 this 会丢失。


watch

侦听器,监听数据变化并执行副作用(异步操作、DOM 操作、开销较大的操作)。

基础用法

js
export default {
  data() {
    return {
      keyword: '',
      selectedId: null,
      user: { name: '', address: { city: '' } },
    }
  },
  watch: {
    // 基本监听
    keyword(newVal, oldVal) {
      console.log(`搜索词: ${oldVal} → ${newVal}`)
      this.fetchResults(newVal)
    },

    // 监听对象的某个属性(字符串路径)
    'user.name'(newVal, oldVal) {
      console.log(`用户名变化: ${newVal}`)
    },

    // 深度监听对象
    user: {
      handler(newVal) {
        console.log('user 对象变化了', newVal)
      },
      deep: true,      // 深度监听
      immediate: true, // 立即执行一次
    },
  },
}

computed vs watch 使用场景

js
// ✅ computed —— 一个值依赖另一个值,纯计算
computed: {
  fullName() {
    return this.firstName + this.lastName
  },
}

// ✅ watch —— 数据变化时执行副作用(发请求、操作 DOM、修改其他数据)
watch: {
  selectedId(newId) {
    this.fetchUserDetail(newId) // 发请求
    this.$router.replace({ query: { id: newId } }) // 修改路由
  },
}

💡 能用 computed 就不用 watch。computed 有缓存,更高效。

watch 的 $watch 实例方法

js
export default {
  mounted() {
    // 动态创建监听器,返回取消函数
    const unwatch = this.$watch(
      'keyword',
      (newVal) => {
        this.fetchResults(newVal)
      },
      { immediate: true }
    )

    // 取消监听
    // unwatch()
  },
}

filters(Vue 2 独有,Vue 3 已移除)

js
// 全局注册
Vue.filter('formatDate', (value, format = 'YYYY-MM-DD') => {
  return dayjs(value).format(format)
})

// 局部注册
export default {
  filters: {
    currency(value, symbol = '¥') {
      return `${symbol}${Number(value).toFixed(2)}`
    },
  },
}
vue
<template>
  <!-- 模板中使用管道符 -->
  <p>{{ date | formatDate('YYYY年MM月DD日') }}</p>
  <p>{{ price | currency('¥') }}</p>
</template>

⚠️ Vue 3 中 filters 被移除,用 computed 或方法代替。


生命周期详解

初始化阶段:
  beforeCreate → created

挂载阶段:
  beforeMount → mounted

更新阶段:
  beforeUpdate → updated

销毁阶段:
  beforeDestroy → destroyed

特殊:
  activated / deactivated  —— keep-alive
  errorCaptured            —— 错误捕获

各阶段详解

js
export default {
  beforeCreate() {
    // data、methods、computed、watch 均未初始化
    // this.$data 为 {}(空对象,尚未与 data 选项合并)
    // 极少使用
    console.log('beforeCreate', this.$data) // undefined
  },

  created() {
    // ✅ data、methods、computed、watch 已初始化
    // ✅ 可以访问 this.xxx、调用 methods
    // ❌ DOM 尚未挂载,不能操作 this.$el
    // 常用场景:发起初始化请求
    this.fetchData()
    this.initWebSocket()
  },

  beforeMount() {
    // 模板编译完成,即将挂载 DOM
    // 很少使用
  },

  mounted() {
    // ✅ DOM 已挂载,可以操作 this.$el、this.$refs
    // 常用场景:操作第三方 DOM 库、初始化图表
    this.initChart()
    this.bindResizeEvent()
  },

  beforeUpdate() {
    // 数据变化后,DOM 更新前
    // 可以访问旧的 DOM 状态
    console.log('旧 DOM:', this.$el.innerHTML)
  },

  updated() {
    // DOM 已更新
    // ⚠️ 避免在此修改数据,可能导致无限循环
    console.log('新 DOM:', this.$el.innerHTML)
  },

  beforeDestroy() {
    // ✅ 组件实例仍可用
    // 常用场景:清理定时器、取消事件监听、断开连接
    clearInterval(this.timer)
    window.removeEventListener('resize', this.handleResize)
    this.socket.close()
  },

  destroyed() {
    // 组件实例已销毁
    // 子组件已销毁,事件监听已解绑
  },

  // keep-alive 相关
  activated() {
    // 从缓存中激活时调用
    // 可重新获取数据
    this.refreshData()
  },
  deactivated() {
    // 进入缓存时调用
    // 清理副作用
    this.stopPolling()
  },

  // 错误捕获
  errorCaptured(err, vm, info) {
    console.error('子组件错误:', err, info)
    // 返回 false 阻止错误继续向上传播
    return false
  },
}

生命周期执行顺序

父 beforeCreate → 父 created → 父 beforeMount
  → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted

💡 父组件等待所有子组件挂载完成后才 mounted。如果需要在父组件 mounted 中操作子组件 DOM,是安全的。


常用实例属性与方法

js
export default {
  mounted() {
    // ---- 数据 ----
    this.$data              // 响应式数据对象
    this.$props             // 当前 props(只读)
    this.$options           // 组件选项对象

    // ---- DOM ----
    this.$el                // 根 DOM 元素
    this.$refs              // 所有 ref 引用(对象)
    this.$refs.input.focus() // 操作 DOM 或子组件

    // ---- 关系 ----
    this.$parent            // 父组件实例
    this.$children          // 子组件实例数组(顺序不确定,不推荐依赖,且非响应式)
    this.$root              // 根组件实例
    this.$slots             // 插槽内容
    this.$scopedSlots       // 作用域插槽
    this.$listeners         // 事件监听器

    // ---- 方法 ----
    this.$emit('change', this.count)  // 触发事件
    this.$set(this.obj, 'key', 'val') // 新增响应式属性
    this.$delete(this.obj, 'key')     // 删除响应式属性
    this.$forceUpdate()               // 强制重新渲染
    this.$nextTick(() => {})          // DOM 更新后执行

    // ---- 全局 ----
    this.$router            // Vue Router 实例
    this.$route             // 当前路由对象
    this.$store             // Vuex Store 实例
    this.$http              // axios(如果挂载到原型)
    this.$message           // UI 库消息组件
    this.$confirm           // UI 库确认弹窗

    // ---- 监听 ----
    this.$watch('count', (newVal, oldVal) => {}, { deep: true })

    // ⚠️ Vue 3 已移除 $on / $once / $off
    // Vue 3 中实例不再支持事件总线模式,需用外部库(如 mitt)替代
    this.$on('event', handler)   // 监听事件(自己触发的)
    this.$once('event', handler) // 只监听一次
    this.$off('event', handler)  // 取消监听
  },
}

常见坑点

1. 箭头函数丢失 this

js
export default {
  methods: {
    // ❌ 箭头函数,this 不是组件实例
    fetchData: () => {
      this.list = []  // this 指向外层作用域
    },

    // ✅ 普通函数
    fetchData() {
      this.list = []
    },
  },
}

2. computed 和 data 同名

js
export default {
  data() { return { name: '张三' } },
  computed: {
    // ❌ 同名会覆盖 data,且报 warning
    name() { return '李四' },
  },
}

3. 模板中直接修改数据

vue
<template>
  <!-- ❌ 避免在模板中执行复杂逻辑 -->
  <p>{{ list.filter(item => item.active).length }}</p>

  <!-- ✅ 用 computed -->
  <p>{{ activeCount }}</p>
</template>

4. v-for 与 v-if 同时使用

vue
<template>
  <!-- ❌ Vue 2 中 v-for 优先级高于 v-if,每个元素都会判断 -->
  <li v-for="item in list" v-if="item.active" :key="item.id">
    {{ item.name }}
  </li>

  <!-- ✅ 用 template 包裹,或用 computed 过滤 -->
  <template v-for="item in list">
    <li v-if="item.active" :key="item.id">{{ item.name }}</li>
  </template>
</template>

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准