data 与方法

data 和 methods 是 Vue 实例最核心的选项。理解它们的工作方式对于构建 Vue 应用至关重要。

data 选项

data 是 Vue 实例的数据对象,Vue 会将其转换为响应式。

对象形式(根实例)

new Vue({
  data: {
    message: 'Hello',
    count: 0,
    user: {
      name: 'John',
      age: 30
    }
  }
})

函数形式(组件)

组件中 data 必须是函数:

Vue.component('my-component', {
  data() {
    return {
      message: 'Hello',
      count: 0
    }
  }
})

为什么必须是函数?因为组件可能被复用多次,函数确保每个实例有独立的数据副本。

响应式数据

data 中的属性是响应式的:

const vm = new Vue({
  data: {
    message: 'Hello'
  }
})

vm.message = 'World'

视图会自动更新。

响应式限制

对象:无法检测新属性

const vm = new Vue({
  data: {
    user: {
      name: 'John'
    }
  }
})

vm.user.age = 30

age 不是响应式的。需要使用 $set:

vm.$set(vm.user, 'age', 30)

或者替换整个对象:

vm.user = { ...vm.user, age: 30 }

数组:无法检测某些操作

const vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})

vm.items[0] = 'x'
vm.items.length = 0

这些操作不会触发更新。应该使用:

vm.items.splice(0, 1, 'x')
vm.items.splice(0)

初始化数据

data 在实例创建时初始化,之后添加的属性不是响应式的:

const vm = new Vue({
  data: {
    message: 'Hello'
  }
})

vm.newProp = 'value'

newProp 不是响应式的。

如果需要动态添加响应式属性,使用 $set:

vm.$set(vm, 'newProp', 'value')

methods 选项

methods 包含实例的方法。

基本用法

new Vue({
  data: {
    count: 0
  },
  methods: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 0
    }
  }
})

this 指向

methods 中的 this 自动绑定到实例:

new Vue({
  data: {
    message: 'Hello'
  },
  methods: {
    greet() {
      console.log(this.message)
    },
    delayedGreet() {
      setTimeout(function() {
        console.log(this.message)
      }, 1000)
    },
    arrowGreet() {
      setTimeout(() => {
        console.log(this.message)
      }, 1000)
    }
  }
})

delayedGreet 会报错,因为普通函数的 this 不是 Vue 实例。使用箭头函数解决。

方法参数

new Vue({
  methods: {
    greet(name) {
      console.log(`Hello, ${name}!`)
    },
    handleClick(event) {
      console.log(event.target)
    }
  }
})

模板中使用:

<button @click="greet('John')">Greet</button>
<button @click="handleClick">Click</button>

方法 vs 箭头函数

不要用箭头函数定义 methods:

new Vue({
  data: {
    message: 'Hello'
  },
  methods: {
    greet: () => {
      console.log(this.message)
    }
  }
})

箭头函数没有自己的 this,会指向外层作用域。

computed 选项

computed 是计算属性,基于依赖缓存。

基本用法

new Vue({
  data: {
    firstName: 'John',
    lastName: 'Doe'
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
})

计算属性 vs 方法

new Vue({
  data: {
    message: 'Hello'
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  },
  methods: {
    getReversedMessage() {
      return this.message.split('').reverse().join('')
    }
  }
})

计算属性基于依赖缓存,只有依赖变化时才重新计算。方法每次调用都执行。

计算属性的 setter

new Vue({
  data: {
    firstName: 'John',
    lastName: 'Doe'
  },
  computed: {
    fullName: {
      get() {
        return `${this.firstName} ${this.lastName}`
      },
      set(value) {
        const [first, last] = value.split(' ')
        this.firstName = first
        this.lastName = last
      }
    }
  }
})

watch 选项

watch 用于监听数据变化。

基本用法

new Vue({
  data: {
    searchQuery: ''
  },
  watch: {
    searchQuery(newVal, oldVal) {
      this.search(newVal)
    }
  },
  methods: {
    search(query) {
      console.log('Searching:', query)
    }
  }
})

深度监听

new Vue({
  data: {
    user: {
      name: 'John',
      age: 30
    }
  },
  watch: {
    user: {
      handler(newVal, oldVal) {
        console.log('User changed')
      },
      deep: true
    }
  }
})

立即执行

new Vue({
  data: {
    userId: 1
  },
  watch: {
    userId: {
      handler(newVal) {
        this.fetchUser(newVal)
      },
      immediate: true
    }
  },
  methods: {
    fetchUser(id) {
      console.log('Fetching user:', id)
    }
  }
})

监听嵌套属性

new Vue({
  data: {
    user: {
      name: 'John'
    }
  },
  watch: {
    'user.name'(newVal, oldVal) {
      console.log('Name changed:', newVal)
    }
  }
})

computed vs watch

使用 computed

当需要根据现有数据计算新值时:

computed: {
  fullName() {
    return `${this.firstName} ${this.lastName}`
  },
  discountedPrice() {
    return this.price * (1 - this.discount)
  }
}

使用 watch

当需要在数据变化时执行异步操作时:

watch: {
  searchQuery(newVal) {
    this.debounceSearch(newVal)
  }
}

组合使用

new Vue({
  data: {
    items: [],
    filter: ''
  },
  computed: {
    filteredItems() {
      return this.items.filter(item => 
        item.name.includes(this.filter)
      )
    }
  },
  watch: {
    filter(newVal) {
      localStorage.setItem('filter', newVal)
    }
  }
})

实际案例

表单处理

new Vue({
  data() {
    return {
      form: {
        username: '',
        email: '',
        password: ''
      },
      errors: {}
    }
  },
  computed: {
    isValid() {
      return (
        this.form.username.length >= 3 &&
        this.form.email.includes('@') &&
        this.form.password.length >= 6
      )
    }
  },
  methods: {
    async submit() {
      if (!this.isValid) return
      
      try {
        await api.register(this.form)
        this.$emit('success')
      } catch (error) {
        this.errors = error.response.data.errors
      }
    },
    reset() {
      this.form = {
        username: '',
        email: '',
        password: ''
      }
      this.errors = {}
    }
  }
})

搜索功能

new Vue({
  data() {
    return {
      query: '',
      results: [],
      loading: false
    }
  },
  computed: {
    hasResults() {
      return this.results.length > 0
    }
  },
  watch: {
    query(newVal) {
      if (newVal.length < 2) {
        this.results = []
        return
      }
      this.search(newVal)
    }
  },
  methods: {
    async search(query) {
      this.loading = true
      try {
        const response = await api.search(query)
        this.results = response.data
      } finally {
        this.loading = false
      }
    }
  }
})

最佳实践

data 初始化

始终在 data 中声明所有需要的属性:

data() {
  return {
    user: null,
    loading: false,
    error: null
  }
}

方法命名

使用动词开头:

methods: {
  fetchData() {},
  handleSubmit() {},
  resetForm() {},
  validateInput() {}
}

计算属性命名

使用名词或形容词:

computed: {
  fullName() {},
  isValid() {},
  filteredItems() {},
  totalPrice() {}
}

避免在 computed 中修改数据

computed: {
  fullName() {
    this.counter++
    return `${this.firstName} ${this.lastName}`
  }
}