阅读视图

发现新文章,点击刷新页面。

回顾计算属性的缓存与监听的触发返回结果

前言

网上的给出的区别有很多,今天只是简单来回归下其中的一些流程和区别的关键点。

以下面的代码为例进行思考:

<template>
  <div class="calc">
    <input v-model.number="n1" type="number" placeholder="数字1">
      <input v-model.number="n2" type="number" placeholder="数字2">
        <p>和:{{ sum }} 平均值:{{ avg }}</p>
        <input v-model="name" placeholder="用户名">
          <p :style="{color: tipColor}">{{ tip }}</p>
        </div>
</template>

<script>
  export default {
    data() {
      return { n1: 0, n2: 0, name: '', tip: '', tipColor: '#333' }
    },
    computed: {
      sum() { return this.n1 + this.n2 },
      avg() { return this.sum / 2 }
    },
    watch: {
      name(v) {
        if (!v.trim()) { this.tip = '不能为空'; this.tipColor = 'red' }
        else if (v.length < 3) { this.tip = '不少于3位'; this.tipColor = 'orange' }
        else { this.tip = '可用'; this.tipColor = 'green' }
      }
    }
  }
</script>

<style scoped>
  .calc { padding: 20px; border: 1px solid #eee; width: 300px; margin: 20px auto; }
  input { display: block; width: 100%; margin: 8px 0; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
</style>

关于 watch 和 computed 的使用我们很多,这里我们不一一介绍,但是请记住:监听是不需要 return 的,计算属性是百分百必须要有的。

1、监听和计算属性的区别

最关键的区别:

1、监听是没有缓存的,计算属性是有缓存的

2、监听是不需要有返回值的,但是机损属性是必须要有返回值的。(极限情况下不return基本没意义)

其他的区别:

对比维度 计算属性(computed) 监听器(watch)
核心用途 基于已有数据推导 / 计算新数据(数据转换 / 组合) 监听已有数据的变化,执行异步操作或复杂逻辑(无新数据产出,侧重 “副作用”)
依赖关系 只能依赖 Vue 实例中的响应式数据(data/props/ 其他 computed),自动感知依赖变化 可监听单个响应式数据、对象属性、数组,甚至通过 deep: true监听对象深层变化,支持手动指定监听目标
使用场景 1. 简单数据拼接(如全名:firstName + lastName)2. 数据格式化(如时间戳转日期字符串)3. 依赖多数据的计算(如总价:price * count)4. 需缓存的重复计算场景 1. 异步操作(如监听输入框变化,延迟请求接口获取联想数据)2. 复杂逻辑处理(如监听用户状态变化,同步更新权限菜单)3. 监听对象深层变化(如监听表单对象,统一处理提交前校验)4. 数据变化后的联动操作(非数据推导类)
是否支持异步 不支持异步操作:若在 computed中使用异步(如定时器、接口请求),无法正确返回推导结果,会得到 undefined 支持异步操作:这是 watch的核心优势之一,可在监听函数中执行任意异步逻辑

2: 监听和计算属性的基本触发流程:

核心逻辑:set → Dep → Watcher → watch/computed 联动流程

无论是 watch 还是 computed,底层联动流程的核心一致,仅在 Watcher 执行逻辑上有差异,完整流程如下:

第一步:初始化阶段 —— 依赖收集(get 拦截器 + Dep + Watcher 绑定)

  1. computed ****的依赖收集
  1. 组件初始化时,会为每个计算属性创建一个「计算属性 Watcher」; 1. 执行计算属性的 get 方法,访问依赖的响应式数据(如 this.num1); 1. 触发该数据的 get 拦截器,get 拦截器会将当前「计算属性 Watcher」添加到该数据的 Dep 依赖列表中; 1. 所有依赖数据都完成 Watcher 绑定,最终缓存计算属性的初始结果。
  1. watch ****的依赖收集
    1. 组件初始化时,会为每个 watch 监听目标创建一个「普通 Watcher」;
    2. 主动读取一次监听目标数据(如 this.username),触发该数据的 get 拦截器;
    3. get 拦截器将当前「普通 Watcher」添加到该数据的 Dep 依赖列表中;
    4. 若开启 deep: true,会递归遍历对象 / 数组的内部属性,完成深层依赖收集。

第二步:更新阶段 ——set 拦截器触发 Watcher 执行

当修改响应式数据时(如 this.num1 = 10),触发底层联动:

  1. 数据被修改,触发该数据的 set 拦截器;
  2. set 拦截器调用对应 Dep 的 notify() 方法(派发更新通知);
  3. Dep 遍历自身的依赖列表,通知所有绑定的 Watcher「数据已更新」;
  4. 不同类型的 Watcher 接收通知后,执行差异化操作(这是 watch 和 computed 表现不同的核心原因):
    • 普通 Watcher (对应 watch :收到通知后,立即执行 watch 的回调函数,传入 newVal 和 oldVal,执行异步 / 复杂逻辑;
    • 计算属性 Watcher (对应 computed :收到通知后,仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行 get 方法计算新结果并更新缓存。

举个例子:

  watch: {
    num(newVal, oldVal) {
      console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
    }
  },

当 set 触发 watcher 后,watcher 就会立即触发:

num(newVal, oldVal) {
      console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
    }

什么叫 收到通知后, 仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行 get 方法计算新结果并更新缓存。

举个例子:

<template>
  <div>
    <!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
    <p v-if="num < 2">两倍数:{{ numDouble }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { num: 1 };
  },
  computed: {
    numDouble() {
      console.log("计算属性执行计算");
      return this.num * 2;
    },
  },
  mounted() {
    setTimeout(() => {
    this.num = 5; // 2秒后,v-if不成立
  }, 2000);
  setTimeout(() => {
    this.num = 1; // 4秒后,v-if再次成立
  }, 4000);
  },
};
</script>

// 控制台只会打印2次“计算属性执行

为什么只打印 2 次:

原因就是我们的计算属性在 2s 后没有执行

1、 当初始化时,页面中 v-if 条件是符合的,会执行一次 get 计算得到返回值

2、当经过两秒后、 v-if 不符合条件,这个时候表明numDouble 是脏数据,会对其进行标记( v-if 不符合条件,所以无法对numDouble 进行访问, 这里也就是我们说的缓存,缓存计算属性的结果值,当脏状态取消时才会进行新的计算 )

3、当 经过 4 秒后条件再次被满足时,才会有新的计算。

3: 计算属性为什么不能异步

举个例子,我们使用延时进行模仿:

<template>
  <div>
    <!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
    <p v-if="num < 2">两倍数:{{ numDouble }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { num: 1 };
  },
  computed: {
    numDouble() {
      setTimeout(() => {
        return this.num * 2;
      }, 1000);
    },
  },
  mounted() {
    console.log(this.numDouble);
  },
};
</script>

打印结果如下:

为什么会打印 underfined 呢,这里有两个原因?

原因 1:JavaScript 中,任何函数如果没有显式写 return 语句,或 return 后没有跟随具体值,都会默认返回 undefined,这是计算属性返回 undefined 的基础原因。

举个例子:

function test() {
        setTimeout(() => {
          return 11;
        }, 1000);
      }
      setTimeout(() => {
        console.log(test());
      }, 2000);

这样写是属于语法的错误。

原因 2:setTimeout 的异步特性(关键)

即使你在 setTimeout 回调中写了 return this.num * 2,这个返回值也毫无意义,因为 setTimeout异步宏任务

  1. 当 Vue 访问 this.numDouble 时,会立即执行计算属性的函数体;
  2. 函数体执行到 setTimeout 时,只会「注册一个异步任务」,然后直接跳过 setTimeout 继续执行;
  3. 此时计算属性函数体已经执行完毕(没有显式 return),默认返回 undefined,并被 console.log 打印;
  4. 1 秒后,setTimeout 的回调函数才会执行,此时回调中的 return this.num * 2 只是回调函数自身的返回值,无法传递给计算属性,也无法改变之前已经返回的 undefined

简单说:异步回调的返回值,无法成为计算属性的返回值,计算属性会在异步任务注册后,直接默认返回 undefined

也可以分两步走

一、先明确:计算属性的函数体,只在 “被访问” 时同步执行一次(除非满足重新计算条件)

当 mounted 中访问 this.numDouble,或者模板渲染访问 numDouble 时,Vue 会同步、完整地执行一遍 ****numDouble ****函数体的代码,但这个执行过程和 setTimeout 内部的回调是完全分离的:

第一步:计算属性函数体「同步执行」(瞬间完成,不等待异步)

我们把 numDouble 的执行过程拆解成 “逐行执行”,你就能看清流程:

numDouble() {
  // 第1步:执行 setTimeout 这行代码
  // 作用:向浏览器“注册一个1秒后执行的异步任务”,仅此而已
  // 注意:这行代码执行时,不会等待1秒,也不会执行回调函数内部的代码
  setTimeout(() => {
    // 这是回调函数,此时完全没有执行!
    console.log("回调函数开始执行");
    return this.num * 2;
  }, 1000);

  // 第2步:计算属性函数体执行到末尾
  // 没有显式 return,默认返回 undefined
  // 此时,numDouble 已经完成了“返回值”的传递,整个函数体执行结束
}

简单来说就是numDouble 函数体的执行,只做了一件事 ——“安排了一个 1 秒后的任务”,然后就直接返回了 undefined,它不会停下来等 1 秒后回调执行完再返回值。

第二步:1 秒后,异步回调才执行,但为时已晚

  1. 计算属性已经在第一步就返回了 undefined,这个返回值已经被 console.log 打印,也被 Vue 缓存起来了;
  2. 1 秒后,浏览器才会执行 setTimeout 的回调函数,此时回调里的 return this.num * 2 只是 “回调函数自己的返回值”—— 这个值没有任何接收者,既不能传给 numDouble ,也不能改变之前已经返回的 undefined
  3. 更关键的是:回调执行时,numDouble 函数体早就执行完毕了,两者是完全独立的执行流程,回调的返回值无法 “回溯” 给已经执行完的计算属性。

简单来讲就是:

计算属性是要内置返回一个结果的,如果加入异步就会因为执行顺讯返回一个undefined,监听是在事件触发后对写入的回调函数的调用。

❌