仅供学习交流,有错误可以评论区指出~

一、什么是 computed 及其用法

computed(计算属性)用于根据已有的响应式数据,声明式地生成新的“派生”数据,并且 Vue 会对其结果做缓存,只有在依赖发生变化时才重新计算。

1. 选项式 API 中的用法

<template>
  <div>
    <p>原始消息:{{ message }}</p>
    <p>反转消息:{{ reversedMessage }}</p>
    <input v-model="message" placeholder="输入一些文字">
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello Vue!'
    };
  },
  computed: {
    // 定义一个计算属性 reversedMessage
    reversedMessage() {
      return this.message.split('').reverse().join('');
    }
  }
};
</script>

效果reversedMessage 会根据 message 自动更新,并且只在 message 变化时才重新执行一次分割与拼接操作。

2. 组合式 API 中的用法

<template>
  <div>
    <p>原始计数:{{ count }}</p>
    <p>双倍计数:{{ double }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const count = ref(0);

// computed 用法与选项式 API 类似
const double = computed(() => count.value * 2);
</script>

二、computed 的实现原理

说在前面,computed 变化要明确2个步骤,一个是依赖是否变化,一个是computed属性的读取。

Vue(以 2.x 为例,3.x 源码思路相似)是基于 响应式系统 + 依赖收集 + watcher 来实现 computed 的。

1. 在 beforeCreate 钩子中初始化
  1. 遍历 computed 配置
    在组件实例化时(beforeCreate 阶段),Vue 会遍历 this.$options.computed 中的每个属性。

  2. 为每个计算属性创建一个专属的 Watcher(computed watcher),lazy为true表示这个 watcher 是“懒执行”的,不会立即调用getter收集依赖,初始时也不会调用 getter,只有在读取时会调用,见第3步

注意:每个计算属性的 Watcher(computed watcher) 会维护两个核心状态:

  • dirty(脏标记,初始为 true
    指示当前缓存值是否被更改,dirty = true 时表示需要重新计算,在下一次读取computed属性时重新计算(调用getter)。

  • value
    用来存储上一次计算的结果。

// 伪代码示意
for (let key in computedOptions) {
  const userDef = computedOptions[key]
  const getter = typeof userDef === 'function'
    ? userDef
    : userDef.get

  // 创建一个 lazy watcher,不立刻执行
  watchers[key] = new Watcher(vm, getter, /* cb */ null, { lazy: true })
  
  // 在实例上代理访问:vm[key] → vm._computedWatchers[key].evaluate()
  defineComputed(vm, key, watchers[key])
}
2.判断依赖是否变化
  • 响应式数据变化通知依赖
    每个响应式数据在值发生改变时,会调用自身的 setter,然后通过 dep.notify() 通知所有收集到的 Watcher,包括计算属性的 Watcher。

  • Watcher 的响应处理
    在所有被通知的 Watcher 中,计算属性的 Watcher 会调用其 update() 方法,内部逻辑类似:

Watcher.prototype.update = function() {
  // 如果是 computed watcher(lazy 为 true),仅标记为 dirty
  if (this.lazy) {
    this.dirty = true;
  } else {
    // 如果是渲染 watcher,则加入更新队列等待组件重新渲染
    queueWatcher(this);
  }
};
  • 计算属性 Watcher:由于其 lazy 属性为 true,所以在依赖数据变化时,只需将内部的 dirty 标记设置为 true,表明下次读取计算属性时需要重新计算。

这种设计保证了计算属性不会在依赖变化时立即重新计算,而是通过 dirty 标记来实现“惰性求值”:只有当计算属性被访问(例如模板渲染或者组件方法中调用)时,才会检查 dirty 状态,并在必要时执行重新计算。

3. 读取计算属性时:依赖收集 + 缓存

无论第1步还是第2步我们都将处理好dirty的值,要么是true要么是false,现在我们可能需要调用computed属性了,这时会检查dirty的值,如果为true就需要更新,这就需要用代理的getter进行计算属性的依赖收集,进行重新计算。

当我们在模板或代码中读取 计算属性 时,实际调用的是代理的 getter,其内部逻辑类似:

Watcher.prototype.evaluate = function() {
  if (this.dirty) {
    // 1. 重新执行用户的 getter
    this.value = this.get();       
    // 2. 依赖收集:在执行 getter 时,
    //    访问的所有响应式属性都会把当前 watcher 加入它们的 Dep
    this.dirty = false;            
  }
  return this.value;
};

function defineComputed(target, key, watcher) {
  Object.defineProperty(target, key, {
    get: function computedGetter() {
      // 触发依赖收集,如果在渲染 watcher 中读取了 computed,
      // 渲染 watcher 也会被收集,保证渲染时的更新
      if (Dep.target) {
        watcher.depend();
      }
      // 计算或直接返回缓存
      return watcher.evaluate();
    },
    set: function computedSetter(newVal) {
      // 如果用户提供了 setter,就调用;否则发出警告
      watcher.userSetter && watcher.userSetter(newVal);
    }
  });
}
  • 首次读取:首次读取计算属性 →发现dirty === true → 调用 getter 计算、收集依赖、缓存结果、dirty = false → 返回 value

  • 后续读取(依赖未变):依赖不改变→读取计算属性 →发现dirty === false → 直接返回缓存的 value,不重复计算。

  • 后续读取(依赖改变):依赖改变→计算属性的watcher将dirty赋值为true→读取计算属性(未来需要使用的时候读取) →发现dirty === true→getter收集依赖更新value。

三、(补充)在模板渲染和改变过程中computed实现

在讲解 模板渲染和改变过程中computed的实现原理之前,先明确一个概念:
当模板(页面)中响应式数据发生变化时,Vue 会涉及两种不同的 Watcher:

  1. 渲染 Watcher(render watcher):负责收集并监听模板中访问的所有响应式依赖,依赖变化时触发组件的重新渲染。

  2. 计算属性 Watcher(computed watcher):负责收集并监听计算属性内部访问的响应式依赖,支持懒计算和结果缓存。

需要特别注意:

  • 首次计算:在组件初次渲染阶段,渲染 Watcher 执行渲染逻辑时会访问计算属性,因为一开始dirty值为true,从而触发计算属性 Watcher 的 getter 进行首次求值,然后dirty被置为false。

  • 后续计算:当依赖变化后,渲染watcher和computed watcher都收到通知,此时渲染watcher直接将DOM的渲染加入队列等待渲染,而computed watcher只会将computed的dirty切换维true,并不做其他操作,等到渲染 Watcher 再次访问该计算属性,才会触发计算属性 Watcher 的 getter 重新计算。

换句话说,计算属性的所有求值操作(包括初始化和依赖更新后的重新计算)都是在渲染 Watcher 执行渲染过程中被调用的。这样既保证了渲染的同步性,又通过计算属性 Watcher 的 lazy + dirty 机制实现了惰性求值和缓存。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐