深入 Vue3 响应式系统:手写 Computed 和 Watch 的奥秘

在 Vue3 的响应式系统中,计算属性和监听器是我们日常开发中频繁使用的特性。但你知道它们背后的实现原理吗?本文将带你从零开始,手写实现 computedwatch,深入理解其设计思想和实现细节。

引言:为什么需要计算属性和监听器?

在Vue应用开发中,我们经常遇到这样的场景:

  • 派生状态:基于现有状态计算新的数据
  • 副作用处理:当特定数据变化时执行相应操作

Vue3提供了computedwatch来优雅解决这些问题。但仅仅会使用还不够,深入理解其底层原理能让我们在复杂场景下更加得心应手。

手写实现Computed

computed的核心特性包括:

  • 惰性计算:只有依赖的响应式数据变化时才重新计算
  • 值缓存:避免重复计算提升性能
  • 依赖追踪:自动收集依赖关系

computed函数接收一个参数,类型函数或者一个对象,对象包含getset方法,get方法是必须得。基本框架就出来了:

js 复制代码
export function computed(getterOrOptions) {
  let getter;
  let setter = undefined;
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
}

当你使用过computed函数时,你会发现会返回一个ComputedRefImpl类型的实例。代码就可以进一步写成下面的样子:

js 复制代码
export class ComputedRefImpl {
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : undefined;
  }
}
export function computed(getterOrOptions) {
  /* 上述代码实现省略 */
  const cRef = new ComputedRefImpl(getter, setter);
  return cRef;
}

ComputedRefImpl的实现

ComputedRefImpl类中有几个主要的属性:

  • _value:缓存的计算结果
  • _v_isRef:表示这是一个ref对象,可以通过.value访问
  • effect 响应式副作用实例
  • _dirty 脏值标记,true表示需要重新计算
  • dep 依赖收集容器,存储依赖当前计算属性的副作用 在初始化的时候,将会创建一个ReactiveEffect实例,此类型在手写Reactive中实现了。
js 复制代码
class ComputedRefImpl {
  effect = undefined; // 响应式副作用实例
  _value = undefined; // 缓存的计算结果
  __v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
  _dirty = true; // 脏值标记,true表示需要重新计算
  dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用

  /**
   * 构造函数
   * @param {Function} getter - 计算属性的getter函数
   * @param {Function} setter - 计算属性的setter函数
   */
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : () => {};

    // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
    this.effect = new ReactiveEffect(getter, () => {
      // 调度器函数 后续处理
    });
  }
}

通过get valueset value手机依赖和触发依赖

js 复制代码
class ComputedRefImpl {
  /* 上述代码实现省略 */
  /**
   * 计算属性的getter
   * 实现缓存机制和依赖收集
   */
  get value() {
    // 如果存在激活的副作用,则进行依赖收集
    if (activeEffect) {
      trackEffects(this.dep || (this.dep = new Set()));
    }

    // 如果是脏值,则重新计算并缓存结果
    if (this._dirty) {
      this._value = this.effect.run(); // 执行getter函数获取新值
      this._dirty = false; // 清除脏值标记
    }

    return this._value; // 返回缓存的值
  }

  /**
   * 计算属性的setter
   * @param {any} newValue - 新的值
   */
  set value(newValue) {
    // 如果有setter函数,则调用它
    if (this.setter) {
      this.setter(newValue);
    }
  }
}

当依赖值发生变化后,将触发副作用的调度器,触发计算属性的副作用更新。

js 复制代码
constructor(getter, setter) {
  this.getter = getter;
  this.setter = isFunction(setter) ? setter : () => {};

  // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
  this.effect = new ReactiveEffect(getter, () => {
    // 调度器函数:当依赖变化时执行
    this._dirty = true; // 标记为脏值,下次访问时需要重新计算
    triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
  });
}

完整代码及用法示例

js 复制代码
import { isFunction } from "./utils";
import {
  activeEffect,
  ReactiveEffect,
  trackEffects,
  triggerEffects,
} from "./effect";

/**
 * 计算属性实现类
 * 负责管理计算属性的getter、setter以及缓存机制
 */
class ComputedRefImpl {
  effect = undefined; // 响应式副作用实例
  _value = undefined; // 缓存的计算结果
  __v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
  _dirty = true; // 脏值标记,true表示需要重新计算
  dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用

  /**
   * 构造函数
   * @param {Function} getter - 计算属性的getter函数
   * @param {Function} setter - 计算属性的setter函数
   */
  constructor(getter, setter) {
    this.getter = getter;
    this.setter = isFunction(setter) ? setter : () => {};

    // 创建响应式副作用实例,当依赖的数据变化时会触发调度器
    this.effect = new ReactiveEffect(getter, () => {
      // 调度器函数:当依赖变化时执行
      this._dirty = true; // 标记为脏值,下次访问时需要重新计算
      triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
    });
  }

  /**
   * 计算属性的getter
   * 实现缓存机制和依赖收集
   */
  get value() {
    // 如果存在激活的副作用,则进行依赖收集
    if (activeEffect) {
      trackEffects(this.dep || (this.dep = new Set()));
    }

    // 如果是脏值,则重新计算并缓存结果
    if (this._dirty) {
      this._value = this.effect.run(); // 执行getter函数获取新值
      this._dirty = false; // 清除脏值标记
    }

    return this._value; // 返回缓存的值
  }

  /**
   * 计算属性的setter
   * @param {any} newValue - 新的值
   */
  set value(newValue) {
    // 如果有setter函数,则调用它
    if (this.setter) {
      this.setter(newValue);
    }
  }
}

/**
 * 创建计算属性的工厂函数
 * @param {Function|Object} getterOrOptions - getter函数或包含get/set的对象
 * @returns {ComputedRefImpl} 计算属性引用实例
 */
export const computed = (getterOrOptions) => {
  let getter; // getter函数
  let setter = undefined; // setter函数

  // 根据参数类型确定getter和setter
  if (isFunction(getterOrOptions)) {
    // 如果参数是函数,则作为getter
    getter = getterOrOptions;
  } else {
    // 如果参数是对象,则分别获取get和set方法
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }

  // 创建并返回计算属性实例
  const cRef = new ComputedRefImpl(getter, setter);
  return cRef;
};

示例用法:

js 复制代码
import { reactive, computed } from "./packages/index";
const state = reactive({
  firstName: "tom",
  lastName: "lee",
  friends: ["jacob", "james", "jimmy"],
});
const fullName = computed({
  get() {
    return state.firstName + " " + state.lastName;
  },
  set(newValue) {
    [state.firstName, state.lastName] = newValue.split(" ");
  },
});
effect(() => {
  app.innerHTML = `
    <div> Welcome ${fullName.value} !</div>
  `;
});
setTimeout(() => {
  fullName.value = "jacob him";
}, 1000);
setTimeout(() => {
  console.log(state.firstName, state.lastName); // firstName: jacob lastName: him 
}, 2000);

手写实现Watch和WatchEffect

watch函数接收三个参数:

  • source:要监听的数据源,可以是响应式对象或函数
  • cb:数据变化时执行的回调函数
  • options 配置选项:immediate:是否立即执行,deep:是否深度监听等
js 复制代码
export function watch(source, cb, {immediate = false} = {}) {
 // 待后续实现
}

1. watch的实现

首先source是否可以接受多种监听的数据源:响应式对象、多个监听数据源的数组、函数。将不同方式统一起来。

js 复制代码
export function watch(source, cb, { immediate = false } = {}) {
  let getter;
  if (isReactive(source)) {
    // 如果是响应式对象 则调用traverse
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    // 如果是函数 则直接执行
    getter = source;
  } else if (isArray(source)) {
    // 处理数组类型的监听源
    getter = () =>
      source.map((s) => {
        if (isReactive(s)) {
          return traverse(s);
        } else if (isFunction(s)) {
          return s();
        }
      });
  }
}
/**
 * 遍历对象及其嵌套属性的函数
 * @param {any} source - 需要遍历的源数据
 * @param {Set} s - 用于记录已访问对象的集合,避免循环引用
 * @returns {any} 返回原始输入数据
 */
export function traverse(source, s = new Set()) {
  // 检查是否为对象类型,如果不是则直接返回
  if (!isObject(source)) {
    return source;
  }
  // 检测循环引用,如果对象已被访问过则直接返回
  if (s.has(source)) {
    return source;
  }
  // 将当前对象加入已访问集合
  s.add(source);
  // 递归遍历对象的所有属性
  for (const key in source) {
    traverse(source[key], s);
  }
  return source;
}

处理完souce参数后,创建一个ReactiveEffect实例,对监听源产生响应式的副作用。

js 复制代码
export function watch(source, cb, { immediate = false } = {}) {
  /* 上述代码以实现省略 */
  let oldValue;
  // 定义副作用执行的任务函数
  const job = () => {
    let newValue = effect.run(); // 获取最新值
    cb(oldValue, newValue); // 触发回调
    oldValue = newValue; // 新值赋给旧值
  };

  // 创建响应式副作用实例
  const effect = new ReactiveEffect(getter, job);
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }
}

⚠️ 性能注意

traverse函数会递归遍历对象的所有嵌套属性,在大型数据结构上使用深度监听(deep: true)时会产生显著性能开销。建议:

  • 只在必要时使用深度监听
  • 尽量使用具体的属性路径而非整个对象
  • 考虑使用计算属性来派生需要监听的数据

2. watchEffect的实现

实现了watch函数后,watchEffect的实现就容易了。

js 复制代码
// watchEffect.js
import { watch } from "./watch";
export function watchEffect(effect, options) {
  return watch(effect, null, options);
}
// watch.js
const job = () => {
  if (cb) {
    let newValue = effect.run(); // 获取最新值
    cb(oldValue, newValue); // 触发回调
    oldValue = newValue; // 新值赋给旧值
  } else {
    effect.run(); // 处理watchEffect
  }
};

用法示例

js 复制代码
watch([() => state.lastName, () => state.firstName], (oldValue, newValue) => {
  console.log("oldValue: " + oldValue, "newValue: " + newValue);
});
setTimeout(() => {
  state.lastName = "jacob";
}, 1000);
setTimeout(() => {
  state.firstName = "james";
}, 1000);
/*
1秒钟后:oldValue: lee,tom newValue: jacob,tom
2秒钟后:oldValue: jacob,tom newValue: jacob,james
*/

总结

本文核心内容

通过手写实现Vue3的computedwatch,我们深入理解了:

  • 计算属性的惰性计算、值缓存和依赖追踪机制
  • 监听器的多数据源处理和深度监听原理
  • 响应式系统中副作用调度和依赖收集的完整流程

代码地址

📝 本文完整代码

GitHub仓库链接\] \| \[[github.com/gardenia83/...](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fgardenia83%2Fvue-source-code "https://github.com/gardenia83/vue-source-code")

下篇预告

在下一篇中,我们将继续深入Vue3响应式系统,手写实现:

《深入 Vue3 响应式系统:从ref到toRefs的完整实现》

  • refshallowRef的底层机制
  • toReftoRefs的响应式转换原理
  • 模板Ref和组件Ref的特殊处理
  • Ref自动解包的神秘面纱

敬请期待! 🚀


掌握底层原理,让我们的开发之路更加从容自信

相关推荐
消失的旧时光-19434 小时前
Kotlinx.serialization 对多态对象(sealed class )支持更好用
java·服务器·前端
少卿5 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技5 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
快起来搬砖了5 小时前
Vue 实现阿里云 OSS 视频分片上传:安全实战与完整方案
vue.js·安全·阿里云
广州华水科技5 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮5 小时前
umi4暗黑模式设置
前端
8***B5 小时前
前端路由权限控制,动态路由生成
前端
军军3605 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1235 小时前
Vue基础知识(一)
前端·javascript·vue.js
terminal0075 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试