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 标志和值缓存,依赖未变时直接返回旧值
  • 惰性求值:只有被访问时才执行计算函数
  • 依赖追踪:自动收集响应式依赖,依赖变化时标记缓存失效
相关推荐
前端付豪2 小时前
NodeJs 做了什么 Fundamentals Internals
前端·开源·node.js
爱分享的鱼鱼2 小时前
Pinia 数据跨组件共享机制与生命周期详解
前端
张元清2 小时前
大白话讲 React2Shell 漏洞:智能家居的语音助手危机
前端·javascript·react.js
wuhen_n2 小时前
手写符合A+规范的Promise
前端
小明记账簿_微信小程序2 小时前
一篇文章教会你接入Deepseek API
前端
若凡SEO2 小时前
深圳优势产业(电子 / 机械)出海独立站运营白皮书
大数据·前端·搜索引擎
踢球的打工仔2 小时前
typescript-void和never
前端·javascript·typescript
hugo_im2 小时前
GrapesJS 完全指南:从零构建你的可视化拖拽编辑器
前端·javascript·前端框架
用户904706683572 小时前
nuxt 路由一篇讲清楚
前端