vue2/3computed原理

computed

computed(计算属性)的核心思想是:基于响应式依赖进行缓存的计算值,只有当依赖发生变化时才会重新计算

Vue2 中 computed 的实现原理

Vue2 的 computed 本质上是一个特殊的 Watcher,具有 lazy(惰性求值)缓存 特性。

js 复制代码
// 简化版 computed 实现
function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null);
  
  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === 'function' 
      ? userDef 
      : userDef.get;
    
    // 为每个计算属性创建一个 computed watcher
    watchers[key] = new Watcher(
      vm,
      getter || (() => {}),
      () => {}, // 回调函数为空,因为 computed 更新由渲染 watcher 触发
      { 
        lazy: true,  // 关键:标记为 computed watcher
        computed: true 
      }
    );
    
    // 将计算属性定义到 vm 实例上
    defineComputed(vm, key, userDef);
  }
}

function defineComputed(target, key, userDef) {
  // 处理 setter(如果提供)
  const setter = userDef.set || (() => {
    console.warn(`Computed property "${key}" was assigned to but has no setter.`);
  });
  
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: setter
  });
}

// 创建计算属性的 getter 函数
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    
    if (watcher) {
      // 关键:只有 dirty 为 true 时才重新计算
      if (watcher.dirty) {
        watcher.evaluate(); // 重新计算值,并标记 dirty 为 false
      }
      
      // 依赖收集:让当前渲染 watcher 收集到这个 computed watcher 作为依赖
      if (Dep.target) {
        watcher.depend();
      }
      
      return watcher.value;
    }
  };
}

// 扩展 Watcher 类以支持 computed
class Watcher {
  constructor(vm, expOrFn, cb, options = {}) {
    this.vm = vm;
    
    // computed watcher 特有的属性
    this.lazy = !!options.lazy;
    this.dirty = this.lazy; // 对于 computed watcher,初始时 dirty 为 true
    this.computed = !!options.computed;
    
    if (this.computed) {
      this.dep = new Dep(); // computed watcher 拥有自己的 dep,用于收集依赖它的渲染 watcher
    }
    
    this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn);
    this.cb = cb;
    
    // 如果不是 lazy watcher,则立即获取值
    this.value = this.lazy ? undefined : this.get();
  }
  
  get() {
    pushTarget(this); // 将当前 watcher 设置为 Dep.target
    let value;
    try {
      value = this.getter.call(this.vm, this.vm);
    } finally {
      popTarget(); // 恢复之前的 watcher
    }
    return value;
  }
  
  evaluate() {
    // 只有 computed watcher 会调用此方法
    this.value = this.get();
    this.dirty = false; // 标记为已计算
  }
  
  depend() {
    // computed watcher 的依赖收集
    if (this.dep && Dep.target) {
      this.dep.depend(); // 让当前 watcher(通常是渲染 watcher)收集这个 computed watcher
    }
  }
  
  update() {
    if (this.lazy) {
      // computed watcher 收到更新通知时,只标记 dirty,不立即重新计算
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this); // 推入异步更新队列
    }
  }
  
  run() {
    const value = this.get();
    if (value !== this.value || this.deep) {
      const oldValue = this.value;
      this.value = value;
      this.cb.call(this.vm, value, oldValue);
    }
  }
}

Vue2 computed 的工作流程

js 复制代码
// 示例代码
new Vue({
  data() {
    return { count: 1, multiplier: 2 };
  },
  computed: {
    total() {
      console.log('计算 total');
      return this.count * this.multiplier;
    }
  },
  template: '<div>{{ total }}</div>'
});

初始化阶段

  1. total 创建 computed watcher ,设置 lazy: true, dirty: true
  2. 不立即计算,等待首次访问

首次渲染阶段

  1. 渲染函数读取 this.total

  2. 触发 computedGetter,发现 watcher.dirty = true

  3. 调用 watcher.evaluate()

    • 执行 this.get(),触发 total 函数
    • 读取 this.countthis.multiplier
    • 将 computed watcher 收集为 countmultiplier 的依赖
    • 计算结果 2,缓存到 watcher.value
    • 设置 dirty = false
  4. 渲染 watcher 通过 watcher.depend() 收集 computed watcher 作为依赖

  5. 返回缓存值 2

依赖更新阶段

  1. this.count = 3 触发 setter
  2. 通知所有依赖的 watcher(包括 computed watcher)
  3. computed watcher 的 update() 被调用,设置 dirty = true
  4. 但此时不会重新计算,只是标记为脏

重新渲染阶段

  1. 渲染函数再次读取 this.total
  2. 触发 computedGetter,发现 watcher.dirty = true
  3. 重新计算,返回新值 6
  4. 设置 dirty = false

Vue3 中 computed 的实现原理

Vue3 的 computed 基于响应式系统的 effectref 实现。

js 复制代码
// 简化版 computed 实现
class ComputedRefImpl {
  constructor(getter, setter) {
    this._setter = setter;
    this._value = undefined;
    this._dirty = true; // 缓存标记
    this._dep = new Set(); // 存储依赖此 computed 的 effect
    this._effect = null;
    
    // 创建响应式 effect
    this._effect = new ReactiveEffect(
      () => {
        // 执行 getter,收集依赖
        return getter();
      },
      () => {
        // 调度器:依赖变化时被调用
        if (!this._dirty) {
          this._dirty = true;
          // 触发依赖此 computed 的所有 effect
          triggerRefValue(this);
        }
      }
    );
    
    this._effect.computed = this;
  }
  
  get value() {
    // 收集依赖
    trackRefValue(this);
    
    // 只有 dirty 时才重新计算
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    
    return this._value;
  }
  
  set value(newValue) {
    this._setter && this._setter(newValue);
  }
}

// 创建 computed
function computed(getterOrOptions) {
  let getter;
  let setter;
  
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions;
    setter = () => {
      console.warn('Write operation failed: computed value is readonly');
    };
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  
  return new ComputedRefImpl(getter, setter);
}

// 依赖收集和触发
function trackRefValue(ref) {
  if (activeEffect) {
    ref._dep.add(activeEffect);
    activeEffect.deps.push(ref._dep);
  }
}

function triggerRefValue(ref) {
  const effects = [...ref._dep];
  for (const effect of effects) {
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect.run();
      }
    }
  }
}

Vue3 computed 的工作流程

js 复制代码
// 示例代码
import { ref, computed } from 'vue';

const count = ref(1);
const multiplier = ref(2);

const total = computed(() => {
  console.log('计算 total');
  return count.value * multiplier.value;
});

// 创建 effect 依赖 total
const stop = effect(() => {
  console.log('total 值:', total.value);
});

初始化阶段

  1. 创建 ComputedRefImpl 实例
  2. 创建 ReactiveEffect,传入 getter 和调度器
  3. 标记 _dirty = true

首次访问阶段

  1. effect 执行,读取 total.value

  2. 触发 get value()

    • trackRefValue(this):将当前 effect 收集到 this._dep
    • 由于 _dirty = true,执行 this._effect.run()
    • 执行 getter,读取 count.valuemultiplier.value
    • computed 的 effect 被收集为 countmultiplier 的依赖
    • 计算结果 2,缓存到 this._value
    • 设置 _dirty = false
  3. 返回 2

依赖更新阶段

  1. count.value = 3 触发响应式更新

  2. 通知所有依赖的 effect(包括 computed 的 effect)

  3. computed effect 的调度器被调用:

    • 设置 _dirty = true
    • triggerRefValue(this):触发依赖此 computed 的所有 effect

重新访问阶段

  1. 依赖 computed 的 effect 重新执行
  2. 再次读取 total.value
  3. 由于 _dirty = true,重新计算,返回 6

总结

Vue的计算属性是一个带缓存的副作用函数,它惰性求值并在依赖未变化时直接返回缓存结果。

这个实现本质可以拆解为三个核心机制:

  • 缓存机制 :内部维护 dirty 标志和值缓存,依赖未变时直接返回旧值
  • 惰性求值:只有被访问时才执行计算函数
  • 依赖追踪:自动收集响应式依赖,依赖变化时标记缓存失效
相关推荐
GISer_Jing43 分钟前
WebGL跨端兼容实战:移动端适配全攻略
前端·aigc·webgl
迦南giser1 小时前
前端性能——传输优化
前端
小白_ysf1 小时前
Vue 中常见的加密方法(对称、非对称、杂凑算法)
前端·vue.js·算法
人工智能训练7 小时前
【极速部署】Ubuntu24.04+CUDA13.0 玩转 VLLM 0.15.0:预编译 Wheel 包 GPU 版安装全攻略
运维·前端·人工智能·python·ai编程·cuda·vllm
会跑的葫芦怪8 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js
pas13611 小时前
40-mini-vue 实现三种联合类型
前端·javascript·vue.js
摇滚侠11 小时前
2 小时快速入门 ES6 基础视频教程
前端·ecmascript·es6
珑墨11 小时前
【Turbo】使用介绍
前端
军军君0112 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three
打小就很皮...13 小时前
Tesseract.js OCR 中文识别
前端·react.js·ocr