不只懂 Vue 语法:试解释 computed、watch 与 methods 的差异?

问题回答

简短答法:computed 最大特点是必须回传一个值,并且会把它缓存起来,当函式里的依赖改变时,才会重新执行和求值。但 watch 与 methods 不会强制要求回传一个值,它们只需执行动作,不一定要回传值。watch 会侦测单一个值,当它有变化时就执行。至於 methods,只要呼叫它,它就会执行,但 computed 和 watch 则不是透过呼叫来执行。

以下会再作详细解说。

computed 的特点

computed 属性的最明显特点:

  • 当元件被创建时(created 生命周期),computed 函式会被建立和执行一次。之後如果依赖没有更新,就不会重新执行和求值,而是回传之前缓存起来的值。
  • computed 的值只能被该 computed 函式修改,不能被其他方法修改,例如 this.某computed 函式 = 123 会报错。
  • computed 的函式必须要回传一个值。
  • computed 的函式无法传入参数。

最後两点很直白,以下就不作解释。第 2 点会涉及到 computed 本身的 getter 机制,明天的文章才会针对作说明。

因此以下先解释第一点。

何谓依赖更新时,才会重新执行?

看看 Vue 官方执行 computed 的解释:

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。

什麽是响应式依赖?意思是在一个 computed 函式里,它所用到在 data 建立的资料。
一旦这些资料有变化,这个 computed 函式就会被重新执行和求值。

举例就,以下 total 的依赖就是 this.price, this.quantity, this.discount。只要其中一样有变化,都会重新执行 total,并回传新的值:

computed:{
    total(){
        return this.price * this.quantity * this.discount
    }
}

以下情况,如果 greeting 有变化,会否触发 total?

computed:{
    total(){
        console.log(this.greeting)
        return this.price * this.quantity * this.discount
    }
}

会。即使 greeting 并没有用来计算 total,但它也是在 total 这个 computed 函式内,因此也是其中一个依赖。

再看一个例子,如果 quantity 有变化,以下会否触发 total?

let quantity = 1;

computed:{
    total(){
        return this.price * quantity * this.discount
    }
}

不会。因为 quantity 不是在 data 里建立,Vue 不会侦察到 quantity 的变化。所以不会触发 total

关於 Vue 如何侦测到 data 资料的变化,可参考此系列之前的文章:
Vue 2:在 Vue 2 为何无法直接修改物件型别资料里的值?
Vue 3:Vue 3 如何使用 Proxy 实现响应式(Reactivity)?

何谓回传之前缓存的值?

意思是当 computed 里的所有依赖都没发生变化,此 computed 函式就会一直回传之前储存起来的值。

举例说,我按下按钮就会把 num 变成 1。

<div id="app">
  <button @click="num = 1">按我改num</button>
  <p> 用add方法把以下的值由0变1:</p>
  <p> {{ add }} </p>
</div>
data(){
    return{
        num: 0,
    }
},
computed:{
    add(){
        console.log('我有被触发了!')
        return this.num
    }
}

刚建立元件时,会印出 「我有被触发了!」。

然後,第一次按下按钮後, num 变成 1,会印出「我有被触发了!」。但之後按按钮,add 就不会再触发,而「我有被触发了!」这句也不会印出来。

因为每次按按钮,都同样是把 num 修改为 1,跟之前储存起来的数值 1 相比并没有变化。因此 add 不会被执行,而画面只会一直显示 1 这个之前已缓存起来的值。

程序码:
https://codepen.io/alysachan/pen/LYWmOLq?editors=1111

无法修改 computed 的值

如果用以下做法修改 computed 的值就会报错:

computed:{
    num(){
        return 1
    }
},
mounted() {
    this.num = 10
}

报错:

[Vue warn]: Write operation failed: computed property "num" is readonly.

因为 computed 的运作原理只有 getter (取值)没有 setter(存值)。因此 computed property 只是唯读。

这部分的解说内容会比较多,留待明天的文章才针对作解释。

methods 的特点

  • methods 函式被呼叫就一定会执行。
  • methods 函式不一定要回传一个值。
  • methods 函式可传入参数。

methods 的概念简单很多。跟平常我们写 JavaScript 时使用函式的概念一样。当它被呼叫时就会执行,可带入参数,而结果不一定要回传一个值,只是执行一些动作也可以。

跟 computed 明显不同,一旦 methods 被呼叫,它不管函式里的所有依赖有没有变化,也会照样执行。

重用以上例子,如果使用 methods 来印出文字:

<div id="app">
  <button @click="add">按我改num</button>
  <p> 用add方法把以下的值由0变1:</p>
  <p> {{ num }} </p>
</div>
data(){
    return{
        num: 0,
    }
},
methods: {
    add(){
        console.log('我是用method,我有被触发了!');
        this.num = 1;
    }
}

程序码:
https://codepen.io/alysachan/pen/BaWxmYz?editors=1111

跟先前的 computed 示范例子的不同:

  • 载入时 console 不会先印出一次文字。
  • num 变成 1 後再按按钮,console 还是会印出文字。

但在画面呼叫 methods,只要资料有变化,它就会被执行?

看看以下例子:

<div id="app">
  <label for="name">输入名称:</label>
  <input type="text" v-model="name" id="name">
  <p> {{ greetUserComputed }} </p>
  <p> {{ greetUserMethods() }} </p>
</div>
data(){
    return{
        greeting: '您好!',
        name: 'Alysa'
    }
},
computed: {
  greetUserComputed() {
    return `${this.name} ${this.greeting}`
  }
},
methods:{
    greetUserMethods(){
        return `${this.name} ${this.greeting}`
    }
}

程序码:
https://codepen.io/alysachan/pen/QWgYGWP?editors=1011

结果是,当我在 input 栏位输入内容,不论 methods 还是 computed 都有被执行。我们知道 computed 一定会被执行,因为依赖出现变化。但为什麽 methods 都会被执行?

Vue 官方文件说明,如果在模版里呼叫 methods 函式,而该函式里的有使用在 data 里建立的资料,一旦这些资料出现变化,就会执行此 methods 函式。


截图自 Vue 官网

因此,在画面呼叫 methods 函式的话,一旦依赖有变化,就会被重新执行。

watch 的特点

  • watch 会侦测某个值,当该值有变化时,就会执行。
  • watch 可传入参数,第一个参数是更新後的值,第二是旧值。
  • 比起 computed,可以处理非同步工作。

前两点都很直白,但对於最後一点的说法,透过例子会更易理解。

computed 无法进行非同步工作,以 AJAX 取资料为例,下面例子是分别以 computed 和 watch 打 API 取得一笔 User 资料。

结果 computed 会回传 [object Promise]。至於 watch,预设是绑定一个空物件,当我按按钮去取资料时,最後就会显示 User 的资料:

<div id="app">
  <button type="button" @click="toggleBtn = true">
    取得 User
  </button>
  <p> {{ userComputed }} </p>
  <p> {{ user }} </p>
</div>
data(){
    return{
      user: {},
      toggleBtn: false,
    }
},
computed:{
  async userComputed() {
    try {
      const res = await axios.get('https://randomuser.me/api/')
      return res.data.results[0]
    }catch(error){
      console.log(error)
    }
  }
},
watch: {
   async toggleBtn() {
     this.user = 'loading...'
     try {
       const res = await axios.get('https://randomuser.me/api/')
       this.user = res.data.results[0]
     }catch(error){
       console.log(error)
     }
   }
}

程序码:
https://codepen.io/alysachan/pen/ZEywejd?editors=1011

结果:

immediatedeep 属性

  • immediate:预设 watch 的函式需要等待它所侦测的值变动时,才会执行。但如果想一载入元件就执行,可以设定 immediate: true
  • deep:当要侦测物件里的属性的变动时,需使用 deep: true

示范:

<div id="app">
  <label for="name">输入名称:</label>
  <input type="text" v-model="user.name" id="name">
</div>
data(){
    return{
      user: {
        name: 'Alysa',
        job: 'web developer'
      }
    }
},
watch: {
  user: {
    handler(newVal, oldVal){
      console.log(oldVal)
      console.log(newVal)
    },
    deep: true,
    // 当元件建立好,就立即执行一次此函式
    immediate: true
  },

  // 无法侦测物件里属性的变动
  // user(newVal, oldVal) {
  //     console.log(oldVal)
  //     console.log(newVal)
  // }

  // 以下是错误写法
  // user(newVal, oldVal) {
  //   deep: true,
  //   immediate: true,
  //   console.log(oldVal)
  //   console.log(newVal)
  // }
}

程序码:
https://codepen.io/alysachan/pen/yLXZbeB?editors=1010

结果是,当使用了 deep 和 immediate 属性,刚载入画面时,console 就会印出数值。之後当我在 input 栏位输入内容,watch 也能侦测到属性的变动,因此再次印出数值。

注意,当你输入了内容,会发现 newVal 和 oldVal 都是一样,因为物件是 pass by reference,所以新旧值都会是相同。

那应该什麽时候用 computed、watch 和 methods?

你会发现,有些情况即使使用 computed、watch 和 methods 都能实现同一效果。但是,computed 的效能通常都会比较好,因为:

  • 减少程序码。
  • 当处理资料量多的资料时,因为缓存资料的机制,效能会比较好。

第一点,官方例子有清晰说明:

截图自 Vue 官网

当使用 watch,就要建立两个函式。反之, 只需建立一个 computed 函式就能完成同样功能。

第二点,因为当依赖没变化时,computed 不会执行,直接回传缓存的值。假设该 computed 函式里每次执行都要跑一个极长的阵列资料,最後回传一笔处理好的阵列。每次执行时会效能耗损都不少。对比起只要一呼叫,就一定会执行的 methods,computed 的缓存机制就比较有利。

总结

computed methods
如果computed的响应式依赖没有改动,就不会触发。 每次触发methodsmethods里的函式一定会执行
有缓存资料的功能 没有缓存资料的功能
不可带入参数 可带入参数
可在HTML里直接使用该computed函式所回传的值,因为computed函式本身是有get函式,最後一定会回传一个值 methods本身没有规定一定要回传一个值

注意:

  • 无法使用 this.computed 函式 = 123 来修改 computed 函式的值。
  • 当在模版中呼叫 methods 时,只要该函式里的依赖有变化,就会被执行。
computed watch
侦测到资料变动时,会回传资料 侦测到资料变动时,不会回传值,只会执行工作
能侦测多个值的变动(只要该资料是在该computed函式里和data属性里) 只能侦测一个值
不能带入参数 能带入参数,第一个参数是新值,第二个参数是旧值

参考资料

Vue 的 computed、methods 跟 watch 差在哪里?(上)
认识 Vue.js watch 监听器


<<:  TailwindCSS 从零开始 - 自订 addBase 套件

>>:  设定字体颜色及文字大小、行距及间距

Day 06 Use automated machine learning in Azure Machine Learning

To use Azure Machine Learning Create an Azure Mach...

【Day13】在Ezyme上装上相对应版本的适配器(Adapter)吧´・ᴗ・`

前面我们有大概提到Enzyme的优点及作用~ 这篇我们要直接来安装Enzyme和导入Enzyme来供...

Day29 小孩子才做选择

Which is better? Modification or construction? 虽然...

day 20 - 新增需求:随时通知目前统计状况 nsq / websocket 介绍

假设收到一个回馈, 希望能即时把目前的点数状况反应在操作画面上, 让调度员可以随时掌握点数的状况来做...

07 - Zim - Zsh 配置框架与它的插件

Zsh 可以让使用者利用配置客制各种不同的功能,像是命令的自动补全、提示、高亮与缩写等。但是要自己设...