Code前端首页关于Code前端联系我们

Vue2里的computed到底怎么用?和methods、watch有啥不一样?

terry 6小时前 阅读数 11 #Vue

不少刚开始学Vue2的同学,提起computed(计算属性)总是又好奇又迷茫:它和methods、watch有啥区别?什么时候必须用它?缓存机制到底咋工作的?这篇文章用问答形式,把computed的核心逻辑、使用场景、避坑技巧全拆明白,哪怕是刚入门的新手,也能一步步搞懂怎么把computed用对、用顺~

computed到底是干啥的?

简单说,computed是Vue里用来基于已有数据动态生成新值的工具,它最大的特点是“依赖缓存”——只有当依赖的数据源变化时,才会重新计算;如果依赖没变化,直接复用之前的计算结果。

举个生活里的例子:你点外卖时,订单总价(computed)依赖菜品单价、数量、优惠折扣(数据源),只要这些数据源不变,总价就不变;一旦某道菜加量、换菜品(数据源变了),总价才会重新算。

再看代码场景:假设要做一个“购物车总价”功能,数据结构长这样👇

data() {
  return {
    goods: [
      { name: '卫衣', price: 129, count: 2 },
      { name: '球鞋', price: 599, count: 1 }
    ],
    discount: 0.9 // 9折优惠
  }
}

要计算最终支付金额,用computed可以这么写:

computed: {
  totalPrice() {
    let sum = 0;
    this.goods.forEach(good => {
      sum += good.price * good.count;
    });
    return sum * this.discount;
  }
}

模板里直接用{{ totalPrice }}就行,这时候只要goods里的price/countdiscount不变,totalPrice就不会重复计算;一旦其中任意一个数据变化,Vue会自动重新计算总价。

要是不用computed,直接在模板里写逻辑(比如{{ goods.reduce(...) * discount }}),不仅模板臃肿难维护,还会每次组件渲染都重复计算——哪怕数据没变化,性能浪费很严重。

computed和methods有啥本质区别?

很多同学最开始会混淆这俩,核心差异在“是否缓存”“触发时机”上。

(1)缓存机制:computed存结果,methods每次执行

methods里的函数,每次被调用时都会重新执行;而computed会把计算结果“存起来”,只有依赖的数据源变化时,才会重新计算。

比如做一个“当前时间格式化”功能:

// methods写法
methods: {
  formatTime() {
    return dayjs().format('YYYY-MM-DD');
  }
}
// computed写法
computed: {
  formattedTime() {
    return dayjs().format('YYYY-MM-DD');
  }
}

模板里如果写{{ formatTime() }}每次组件渲染(哪怕数据没变化)都会执行formatTime函数;但如果写{{ formattedTime }}只有当formattedTime依赖的数据源变化时才会重新计算,但这里有个小陷阱:dayjs()是“非响应式”数据(不是data里的变量),所以formattedTime的依赖其实没变化,它会一直返回第一次计算的结果——这也侧面说明computed的缓存是基于“响应式数据源”的(后面会细讲)。

(2)性能差异:循环场景下天差地别

如果在v-for循环里调用方法,性能问题会被放大,比如渲染100条数据,每条都调用methods里的函数:

<ul>
  <li v-for="item in list" :key="item.id">
    {{ formatItem(item) }} 
  </li>
</ul>

只要组件重新渲染(比如父组件传参变化、其他数据更新),formatItem会被调用100次;但如果用computed先把所有格式化后的结果生成好,再渲染:

computed: {
  formattedList() {
    return this.list.map(item => this.formatItem(item));
  }
},
methods: {
  formatItem(item) { /* 格式化逻辑 */ }
}

模板里用v-for="item in formattedList"只有list变化时,formattedList才会重新计算,渲染时直接拿缓存结果,性能好太多。

(3)适用场景:一个“算值”,一个“干活”

总结下选择逻辑:

  • computed:依赖其他数据推导新值,且希望结果被缓存(比如过滤列表、计算总价、数据格式化)。
  • methods:需要主动触发执行(比如点击事件),或不需要缓存的一次性操作(比如表单提交、事件处理函数)。

computed的缓存是怎么工作的?哪些情况会让它重新计算?

理解computed的缓存逻辑,得先懂Vue的“响应式依赖收集”机制:

  1. 当执行computed的getter函数时,Vue会自动“收集”里面用到的响应式数据(比如data、props里的变量)作为“依赖”。
  2. 当这些依赖的数据发生变化时,Vue会标记这个computed为“脏”(需要重新计算)。
  3. 下次访问这个computed属性时,才会真正重新计算,并更新缓存结果。

举个例子:

data() {
  return {
    a: 1,
    b: 2
  }
},
computed: {
  sum() {
    return this.a + this.b; // 收集a和b作为依赖
  }
}

ab变化时,sum会被标记为脏;但如果只是访问sum,而ab没变化,sum会直接返回之前缓存的结果。

(1)“非响应式数据”不会触发重新计算

如果computed里用了非响应式数据(比如普通变量、window对象、第三方库的非响应式数据),Vue没法收集到依赖,所以数据变化时computed不会更新。

比如这样写就会踩坑:

let num = 1; // 普通变量,非响应式
data() {
  return { a: 2 }
},
computed: {
  result() {
    return num + this.a; // num不是响应式数据
  }
}

哪怕后面执行num = 3result也不会重新计算——因为Vue没把num当成依赖,解决方法:把num放到data里(变成响应式),或者用watch监听num的变化(但num如果是外部变量,watch也监听不到,所以最好把需要响应的数放data/props)。

(2)数组/对象的“变异”要注意

Vue2里,数组的索引修改、长度修改(比如arr[0] = 1arr.length = 0不会触发响应式更新,所以如果computed依赖这样的数组,修改方式不对就不会重新计算。

正确做法是用数组的变异方法push/pop/splice等),或者用this.$set修改对象属性。

data() {
  return {
    list: [1, 2, 3]
  }
},
computed: {
  listSum() {
    return this.list.reduce((t, v) => t + v, 0);
  }
}
// 错误修改:直接改索引
this.list[0] = 10; // listSum不会更新
// 正确修改:用splice
this.list.splice(0, 1, 10); // listSum会更新

computed里能写异步操作吗?为啥?

不建议在computed里写异步,甚至可以说“不能正确工作”,原因和computed的设计逻辑有关:

computed的getter函数需要返回一个确定的值,用来做缓存和依赖收集,但异步操作(比如axios请求、setTimeout)的结果是“延迟返回”的,getter执行时拿不到异步结果,只能返回undefined或者初始值,后续异步完成时,computed的依赖机制也没法感知到变化,导致缓存失效或结果错误。

举个反例:想通过异步请求获取用户信息,然后计算用户名:

computed: {
  userFullName() {
    let name = '';
    axios.get('/user').then(res => {
      name = res.data.first + res.data.last;
    });
    return name; // 这里返回的是初始值'',异步结果拿到后也没法更新computed
  }
}

模板里显示{{ userFullName }}永远是空字符串,因为getter执行时异步还没完成,后续异步完成后,computed也不会重新计算。

那要处理异步场景咋办?推荐用watch + data或者methods

data() {
  return {
    user: null,
    fullName: ''
  }
},
watch: {
  // 监听user变化,异步请求后更新fullName
  user(newVal) {
    axios.get(`/user/${newVal.id}`).then(res => {
      this.fullName = res.data.first + res.data.last;
    });
  }
},
methods: {
  async fetchUser() {
    const res = await axios.get('/currentUser');
    this.user = res.data;
  }
}

这样user变化时,watch触发异步请求,更新fullNamefullName可以是普通data,也可以是computed依赖的数据源),逻辑更可控。

computed的setter怎么用?平时用得多吗?

默认情况下,computed只有getter(用来获取值),但也可以手动定义setter(用来设置值),当你给computed属性赋值时,setter会被触发。

(1)setter的基本用法

语法长这样:

computed: {
  fullName: {
    get() {
      return this.firstName + ' ' + this.lastName;
    },
    set(newValue) {
      // 拆分newValue到firstName和lastName
      const [first, last] = newValue.split(' ');
      this.firstName = first;
      this.lastName = last;
    }
  }
}

当执行this.fullName = 'John Doe'时,setter会被调用,firstNamelastName会被更新,进而触发fullName的getter重新计算。

(2)实际用例:表单双向绑定、数据同步

setter在“一个值的变化需要同步修改多个数据源”的场景很有用,

  • 表单里的“全名”输入框,需要拆分到firstNamelastName两个字段(用户输入全名,自动拆分存到两个变量)。
  • 父子组件传值时,子组件通过computed的setter同步修改父组件传的props(结合v-model语法糖)。

举个表单例子:

<template>
  <input v-model="fullName" placeholder="输入全名"/>
  <p>姓:{{ firstName }}</p>
  <p>名:{{ lastName }}</p>
</template>
<p><script>
export default {
data() {
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName: {
get() {
return this.firstName + ' ' + this.lastName;
},
set(val) {
const [first, last] = val.split(' ');
this.firstName = first || '';
this.lastName = last || '';
}
}
}
}
</script>

用户输入“Alice Bob”,firstName会变成Alice,lastName变成Bob,模板里实时显示拆分结果。

(3)使用频率:getter远多于setter

日常开发中,getter的使用场景占90%以上,setter只有在需要“反向修改多个数据源”时才用,如果只是简单的“计算值”,用getter足够;复杂的双向同步逻辑,再考虑setter + 合理的依赖管理。

computed和watch怎么选?

很多同学分不清这俩,核心看“目的是‘算值’还是‘执行副作用’”

(1)computed:专注“推导新值”(多对一)

computed是“基于多个依赖,生成一个新值”,重点在“值的推导”,而且结果会被缓存。

  • 根据搜索关键词过滤列表(依赖列表和关键词,生成过滤后的列表)。
  • 根据用户选择的商品、数量、优惠,计算最终价格(多依赖→单值)。

(2)watch:专注“响应变化,执行操作”(一对一/多对多)

watch是“监听一个或多个数据的变化,执行副作用(比如异步请求、DOM操作、复杂逻辑)”,重点在“执行操作”,没有缓存(每次变化都触发)。

  • 用户登录状态变化时,跳转页面并请求用户信息(监听isLogin,执行路由跳转和axios请求)。
  • 多个表单字段变化时,实时验证表单(监听usernamepassword等,执行验证逻辑)。
  • 监听路由参数变化,重新获取页面数据(比如商品ID变化,重新请求商品详情)。

(3)举个对比例子:搜索功能

需求:用户输入关键词,实时过滤商品列表。

  • 用computed:把过滤逻辑放computed里,依赖列表和关键词,返回过滤后的列表,模板渲染过滤后的列表,自动响应数据变化。
  • 用watch:监听关键词变化,每次变化时执行过滤函数,把结果存到data里,模板渲染这个data里的过滤后列表。

两种方式都能实现,但computed更简洁(自动缓存、自动响应),watch需要手动维护data和逻辑,所以这种“推导值”的场景优先选computed。

再比如“用户选择城市后,请求该城市的天气数据”:这种“数据变化→执行异步

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门