跳到主要内容位置

Vue computed 计算属性和 watch 监听器的区别

张旭乾

计算属性和 watcher 监听器都可以监听 data 中的数据的变化,然后根据具体的业务逻辑对数据进行操作。它们两个的区别是,computed() 计算属性一般是把 data 中的数据进行一番包装和计算之后,返回新的值,例如对数组进行过滤后的值,而并不会直接修改 data 中的属性:

data() {
return {
blogPosts: [
"Vue 3.0x 入门实战",
"Vue 3.x 完全指南",
"React 18 新特性介绍",
"JavaScript 基础语法概览",
],
};
},
computed: {
count() {
return this.blogPosts.length;
},
},

之后在 html 模板中使用计算属性时,能跟像使用 data 属性一样的方式进行使用。对 data 中属性的修改能立即反应到计算属性上,html 模板也能立即更新:

<ul>
<li v-for="blog in blogPosts">{{ blog }}</li>
</ul>
<h1>共({{ count }})篇博客</h1>
<button @click="blogPosts.push('Vue 3.x 计算属性和监听器的区别')">
添加一篇博客
</button>

而 watch 监听器则是监听到某个属性变化的时候,根据业务逻辑,去直接修改 data 中的其它属性值,它不会直接返回计算结果,也不能直接从 html 中使用它:

data() {
return {
blogPosts: [
"Vue 3.0x 入门实战",
"Vue 3.x 完全指南",
"React 18 新特性介绍",
"JavaScript 基础语法概览",
],
count: 4,
};
},
watch: {
blogPosts: {
handler(newVal) {
this.count = newVal.length;
},
deep: true,
},
}

<ul>
<li v-for="blog in blogPosts">{{ blog }}</li>
</ul>
<h1>共({{ count }})篇博客</h1>
<button @click="blogPosts.push('Vue 3.x 计算属性和监听器的区别')">
添加一篇博客
</button>

正因为如此,我们需要在 data 属性中额外维护一个属性,来让 HTML 模板使用,在 watch 监听到特定的属性变化时,再去修改相关属性的值。

这里的示例代码注意一下 watch 的新语法,这里使用了对象语法,而不是函数语法,这是因为数组是按引用比较的,即使里边的元素发生了变化,但数组本身的引用地址没有变化,所以使用函数的形式不会监听到数组的变化,这里使用对象形式,设置 deep 为 true,可以比较数组内部元素的变化,这样就能让监听器生效了,同时处理监听的函数固定变成了名字为 handler 的函数,它也接收新值和旧值作为参数。

那么根据这个特性,watch 就更适合耗时的操作,在属性变化的时候(例如事件处理中),我们把耗时的逻辑放到 watch 监听器里去异步的去执行,什么时候执行完了,它会自动更新 data 中的属性值(这种不返回新的数据,直接在函数内部修改 data 属性的这种方式,是命令式的, Imperative):

data() {
return {
blogPosts: [
"Vue 3.0x 入门实战",
"Vue 3.x 完全指南",
"React 18 新特性介绍",
"JavaScript 基础语法概览",
],
newBlog: "", // 使用 watch 监听
count: 4,
};
},
watch: {
newBlog(newVal) {
// 模拟耗时操作
setTimeout(() => {
this.blogPosts.push(newVal);
this.count += 1;
}, 2000);
},
}

<ul>
<li v-for="blog in blogPosts">{{ blog }}</li>
</ul>
<h1>共({{ count }})篇博客</h1>
<button @click="newBlog = 'Vue 3.x 计算属性和监听器的区别'">
添加一篇博客
</button>
{{ newBlog }}

不过,异步操作和修改 data 中的属性并不是 watch 监听器的专利,在计算属性中也可以,但是要记住这种方式尽量要避免,虽然可以这样做,但并不代表这么做就是好的。这种方式利用计算属性的 getter 和 setter 的形式,我们之前使用函数定义 computed 计算属性的方式就相当于只提供了 getter,可以在 html 中访问数据,不过呢,它还可以设置 setter:

data() {
return {
blogPosts: [
"Vue 3.0x 入门实战",
"Vue 3.x 完全指南",
"React 18 新特性介绍",
"JavaScript 基础语法概览",
],
// newBlog: "",
newBlogPost: "", // 使用 computed 的方式,改了名字防止和计算属性混淆
};
},
computed: {
newBlog: {
get() {
return this.newBlogPost;
},
set(value) {
this.newBlogPost = value; // 需要手动赋值,无法访问之前的值,烦琐
setTimeout(() => {
this.blogPosts.push(value); // 虽然代码中可以修改其它属性,但推荐只对它所计算的属性进行修改,并且不要进行异步的修改。
}, 2000);
},
},
},

使用对象的 getter 和 setter 的形式,setter 可以接收一个参数,也就是新的值,如果对计算属性进行了修改,例如在事件处理中,那么这个新值就传递给了 setter ,在它里边可以根据新值,来修改 data 中其它属性的值:

<ul>
<li v-for="blog in blogPosts">{{ blog }}</li>
</ul>
<h1>共({{ count }})篇博客</h1>
<button @click="newBlog = 'Vue 3.x 计算属性和监听器的区别'">
添加一篇博客
</button>
{{ newBlog }}

这里虽然可以和 watch 一样,根据自身的变化,修改其它属性的值,但是 computed 计算属性的 setter 应该只对自己本身所计算的属性负责,只修改和计算属性相关的属性,不要修改其它属性,这样就产生了所谓的副作用(Side Effect)。所以在监听某个属性的变化,并修改其它的属性时,尽量使用 watch 监听器。而根据某个属性的变化返回计算结果的时候,要使用计算属性。

Computed 和 Watch 的区别

ComputedWatch
简单的业务逻辑计算耗时的操作和远程 API 加载
可以直接在 HTML 模板中使用不能在 HTML 模板中使用
响应 data 数据变化响应数据变化
有返回值/getter没有返回值
也可以使用 setters 修改 data 中的数据可以修改 data 中的数据

示例

我们来看一下计算属性和监听器的示例,来看看它们实际使用和运行时的区别。假设我们的页面上会显示当前博客文章的数量,并且有一个按钮来添加一篇新的博文到博客数组中,那么我们打开 index.js 文件,先定义一个计算属性,显示博客文章数量,这里在 computed 配置对象里,添加一个 count 方法,返回 blogPosts 数组的长度:

computed: {
count() {
return this.blogPosts.length;
},
},

然后打开 index.html,我们这里的 ul 列表显示所有的文章,遍历 blogPosts 数组,再在 ul 列表的下方,使用一个 h1 来显示文章数量:

<ul>
<li v-for="blog in blogPosts">{{ blog }}</li>
</ul>
<h1>共({{ count }})篇博客</h1>

再在 h1 的下方添加一个按钮,文本标签为添加一篇博客,然后给它添加一个点击事件监听 @click,这里直接写上一串 JavaScript 代码,添加 1 个新的元素到 blogPosts 数组中:

<button @click="blogPosts.push('Vue 3.x 计算属性和监听器的区别')">
添加一篇博客
</button>

好,我们运行一下项目,可以看到现在的博客数量为 4,点击一下按钮,可以看到博客列表里新增了一篇文章,同时文章数量也变成了 5。 接下来我们看一下同样的功能,用 watch 监听器是怎么实现的。打开 index.js 文件,因为 watch 没有办法直接返回值,所以我们需要在 data 里再定义一个 count 属性,用于表示博客的数量,默认值为 blogPosts 数组的长度,我们这里直接把它写为 4:

data() {
return {
blogPosts: [],
count: 4, // 使用 watch 的方式
};
},

接着注释掉 computed 计算属性里的 count 方法:

// count() {
// return this.blogPosts.length;
// },

再添加一个 watch 配置项,在里边我们监听 blogPosts 数组的变化,之前我们讲了要监听数组元素的变化,需要设置 deep 为 true,这时需要使用对象的形式来监听属性,这里我们添加一个 blogPosts 属性,它的名字仍然需要和要监听的属性名保持一致,然后值是一个对象,对象里设置 deep 为 true,然后处理函数的名字固定为 handler,在里边我们获取新数组的长度,并把它赋值给 count 属性:

  watch: {
blogPosts: {
handler(newVal) {
this.count = newVal.length;
},
deep: true,
},
},

这样,我们在运行一下示例,可以看到功能是一样的,但是这样操作就复杂多了,又要监听 blogPosts 的变化,又要手动修改 count 属性的值,完全不如计算属性方便快速。 watch 真正的用途是异步的执行耗时的操作,我们把这个示例改一下,假设点击按钮的时候不是直接添加一篇文章到数组中,而是设置新添加的文章的标题,过两秒之后再添加到数组中,我们来看一下怎么实现。打开 index.js 文件,我们先把 data 中的 count 删掉,然后使用 computed 计算属性中的 count,这种比较推荐的形式:

// count: 4, // 使用 watch 的方式 // computed count() { }

再在 data 中再添加一个 newBlog 属性,保存新添加的博客标题,初始值为空:

newBlog: "",

然后修改按钮的点击事件,设置 newBlog 的属性值,我们还可以在按钮的下方访问 newBlog 的值,来方便我们观察它的变化:

<button @click="newBlog = 'Vue 3.x 计算属性和监听器的区别'">
添加一篇博客
</button>
{{ newBlog }}

之后在 watch 里边,监听 newBlog 属性的变化,这里使用普通的函数形式就可以了,当 newBlog 属性变化时,使用 setTimeout 推迟代码的执行,模拟一个耗时的操作,定时 2 秒,在到期后,把新的博客添加到 blogPosts 数组中:

  // watch
newBlog(newVal) {
// 模拟耗时操作
setTimeout(() => {
this.blogPosts.push(newVal);
}, 2000);
},

现在再运行一下项目,点击一下按钮,可以看到 newBlog 的值发生了变化,等过 2 秒之后,新的博客文章就添加到了上方的列表中,数量也变成了 5。这种操作一般使用计算属性是无法实现的,但是通过计算属性的 setter ,也能实现相同的功能,这种方式十分不推荐,不过既然 Vue 提供了这个功能,我们就来看一下它的使用方法。为了避免 data 中的属性和计算属性的 getter 和 setter 同名,我们把 data 中的 newBlog 改成 newBlogPost:

newBlogPost: "",

然后在 computed 配置对象里,添加一个 newBlog 属性,它的值是一个对象,我们在里边配置 getter 和 setter,getter 就是一个名字为 get 的函数,在里面我们返回 newBlogPost 的值:

newBlog: {
get() {
return this.newBlogPost;
},
},

这时访问 newBlog 属性的值就是访问的 newBlogPost 的值,我们再给它添加一个 setter,也就是一个名字为 set 的函数,当修改 newBlog 这个计算属性的值时,会把新值通过参数传递进来,我们可以根据它做一些额外的操作,例如这里我们先把 newBlogPost 的值设置为新值,之后像 watch 一样,使用一个 setTImout() 模拟耗时的操作,把新添加的博客添加到 blogPosts 数组中:

newBlog: {
get() {
return this.newBlogPost;
},
set(value) {
this.newBlogPost = value;
setTimeout(() => {
this.blogPosts.push(value);
}, 2000);
},
},

html 模板中的代码没有变化,我们运行一下项目看一下效果,可以看到和使用 watch 的方式是一样的。使用 setter 形式的缺点是,需要手动维护属性的赋值和访问,比使用 watch 的方式代码量要多不少,而且在里边修改了其它的属性时,例如 blogPosts 数组,那么就产生了副作用,这个不是计算属性设计的初衷,还是应尽量使用函数的形式,根据 data 中的某个属性值来计算出新的值。

小结

好了,这个就是 computed 计算属性和 watch 监听器之间的区别:

  • computed 计算属性适合根据 data 里的属性,来做一些简单的计算并返回结果,例如数组的排序、筛选等等,它的结果会缓存起来,只有 data 中的属性发生变化时才会重新计算,其它情况会直接返回计算结果,以提高效率。计算属性可以像 data 属性一样直接在 html 模板中使用。
  • watch 适合在 javascript 中监听 data 属性的变化,并根据变化做一些耗时的操作或者发送远程 API 请求。watch 中的方法一般没有返回值,而是直接修改 data 中的属性。
提示

《Vue 3.x 全家桶完全指南与实战》课程已上线,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。

《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。

新书《JavaScript 基础语法详解》已上架,可在京东、当当、淘宝等各大电商购买