重学 Vue3,深入响应式原理:从 ref 和 computed 的底层实现开始!

1 阅读8分钟

一、引言:为什么需要深入响应式原理?

在 Vue3 的世界中,响应式系统是整个框架的核心引擎。相比 Vue2 基于 Object.defineProperty 的实现,Vue3 使用 Proxy 彻底重构了响应式系统,带来了显著的性能提升和更强大的功能。

理解响应式原理的底层实现,能让你:

  1. 优化性能:避免不必要的重新渲染和计算
  2. 规避陷阱:识别常见反模式(如循环依赖、无效更新)
  3. 提升心智模型:写出更符合响应式思维的代码
  4. 掌握高级特性:灵活运用响应式API解决复杂问题

本文将深入剖析 refcomputed 这两个最常用的响应式API的底层实现,揭示Vue3响应式系统的设计哲学。

二、前置知识:响应式系统的基石

1. 核心概念回顾

  • 副作用(Effect) :任何会改变程序状态的操作(如DOM更新)
  • 依赖收集(Track) :追踪当前副作用依赖的数据
  • 触发更新(Trigger) :当数据变化时通知相关副作用重新执行

2. 底层思路解析

Vue3 响应式系统建立在现代JavaScript Proxy特性之上:

// Proxy 基础示例
const data = { count: 0 };
const proxy = new Proxy(data, {
  get(target, key) {
    track(target, key); // 依赖收集
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    trigger(target, key); // 触发更新
    return true;
  }
});

依赖存储的核心数据结构是嵌套的Map集合:

// 全局依赖存储结构
const targetMap = new WeakMap();

// 依赖收集伪代码
function track(target, key) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set())); 
  }
  
  dep.add(activeEffect);
}

这种 WeakMap → Map → Set 的三层结构能高效管理依赖关系,同时避免内存泄漏。

三、ref 的魔法:基础类型的响应式方案

1. 为什么需要 ref?

ref 解决了响应式系统中的几个关键问题:

  • 基本类型值(number, string等)的响应式包装
  • DOM元素引用的响应式管理
  • 性能隔离(独立于大型reactive对象)

2. 源码实现解密

下面是简化版的 ref 实现:

class RefImpl {
  constructor(value) {
    this._rawValue = value; // 存储原始值
    this._value = convert(value); // 值转换(对象转为reactive)
    this.dep = undefined; // 依赖收集器
  }

  get value() {
    trackRefValue(this); // 触发依赖收集
    return this._value;
  }

  set value(newVal) {
    // 使用 Object.is 进行严格比较
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = convert(newVal);
      triggerRefValue(this); // 触发依赖更新
    }
  }
}

// 辅助函数
function convert(value) {
  return isObject(value) ? reactive(value) : value;
}

function hasChanged(value, oldValue) {
  return !Object.is(value, oldValue);
}

3. 关键机制详解

依赖收集过程

function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = new Set()));
  }
}

function trackEffects(dep) {
  dep.add(activeEffect);
}

值变更检测

自动解包原理

在模板中使用时,Vue编译器会自动添加 .value 访问,开发者无需手动编写.

注意事项,参考官方文档:在模板中解包的注意事项

四、computed:惰性求值与缓存的艺术

1. 设计哲学解析

计算属性的核心价值在于:

  • 惰性计算:只在需要时执行
  • 结果缓存:依赖未变化时直接返回缓存值
  • 依赖追踪:自动管理依赖关系

2. 源码架构解剖

class ComputedRefImpl {
  constructor(getter) {
    this._dirty = true; // 脏检查标志
    this._value = undefined;
    // 创建副作用函数
    this.effect = new ReactiveEffect(
      getter,
      () => {
        // 调度器:依赖变更时触发
        if (!this._dirty) {
          this._dirty = true; // 标记需要重新计算
          triggerRefValue(this); // 触发依赖更新
        }
      }
    );
  }

  get value() {
    // 收集访问计算属性的副作用
    trackRefValue(this);
    
    // 当值需要更新时重新计算
    if (this._dirty) {
      this._dirty = false;
      this._value = this.effect.run();
    }
    return this._value;
  }
}

// 简化的副作用类
class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn;
    this.scheduler = scheduler;
  }

  run() {
    activeEffect = this;
    const result = this.fn();
    activeEffect = undefined;
    return result;
  }
}

3. 核心特性实现

缓存机制工作原理

依赖更新流程

  1. 依赖项改变,触发调度器
  2. 设置 _dirty = true(标记需要重新计算)
  3. 通知所有依赖此计算属性的副作用
  4. 当副作用重新执行时,访问计算属性触发重新计算

五、响应式系统的协同作战

1. ref 与 computed 的联动

典型场景:ref → computed → 组件渲染

const count = ref(0);
const double = computed(() => count.value * 2);
// 依赖关系:
// count → double → 渲染函数

count.value 改变时:

  1. 触发 count 的 setter
  2. 通知 double 的调度器
  3. double 标记为 dirty
  4. 通知渲染函数重新执行
  5. 渲染函数访问 double.value 触发重新计算

2. 内存管理揭秘

Vue3 使用 WeakMap 避免内存泄漏的关键:

// 全局依赖存储
const targetMap = new WeakMap();
// 当 target 不再被引用时
// WeakMap 中的条目会被自动垃圾回收

依赖自动清除机制:

// 组件卸载时
function cleanup(effect) {
  for (const dep of effect.deps) {
    dep.delete(effect);
  }
  effect.deps.length = 0;
}

六、实战中的高级技巧

1. 性能优化策略

避免 computed 的过度计算

// 反模式:每次访问都重新计算
const expensive = computed(() => {
  return hugeArray.value.filter(...).map(...)
});

// 优化:添加条件判断
const filtered = computed(() => {
  if (!shouldCompute.value) return cachedResult;
  return hugeArray.value.filter(...);
});

合理使用 shallowRef

// 大型对象优化
const bigData = shallowRef({ ... });

// 需要更新时
bigData.value = { ...bigData.value, updatedProp: 'new' };

2. 陷阱规避指南

避免 .value 丢失响应性

// 错误:解构失去响应性
const { value: count } = ref(0);
// 正确:保持引用
const countRef = ref(0);
const count = countRef.value; // 仅用于读取

解决循环依赖问题

const a = ref(1);
const b = computed(() => a.value + 1);

// 循环依赖导致死锁
a.value = b.value; // 错误!

// 解决方案:使用 nextTick
import { nextTick } from 'vue';

nextTick(() => {
  a.value = b.value;
});

七、延伸思考:响应式系统的未来

1. Vue Reactivity 的独立性

Vue3 的响应式系统已解耦为独立库( @vue/reactivity),可在任何JS环境中使用:

import { ref, computed } from '@vue/reactivity';
// 在非Vue项目中使用
const state = ref(0);
const double = computed(() => state.value * 2);

2. 响应式编程范式的扩展

  • 状态机:基于响应式的状态管理
  • 时间旅行:响应式数据+快照实现状态回溯
  • 响应式UI:将UI视为数据的函数

3. 与其他框架对比

特性Vue3 ReactivityMobXSolidJS
实现原理ProxyProxy编译时转换
细粒度更新中等极高
学习曲线平缓中等陡峭
包大小轻量(6kb)中等(16kb)极小(3kb)

八、总结:重新理解响应式

通过深入 refcomputed 的底层实现,我们揭示了Vue3响应式系统的核心设计:

  1. ref 通过类封装和值检测,解决了基本类型的响应式问题
  2. computed 利用脏检查机制和调度器,实现了惰性求值和缓存
  3. 依赖管理 基于WeakMap的三层结构,高效且内存安全
  4. 响应式协同 通过精妙的依赖链通知机制,实现高效更新

理解这些底层原理不仅能帮你写出更高效的Vue代码,还能培养响应式编程思维。我强烈建议你在实际项目中尝试调试响应式系统:

// 调试小技巧
import { ref, onRenderTracked, onRenderTriggered } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    onRenderTracked((e) => {
      console.log('依赖收集:', e);
    });
    
    onRenderTriggered((e) => {
      console.log('依赖触发:', e);
    });
    
    return { count };
  }
};

响应式系统是Vue的灵魂所在,深入理解它将使你从Vue的使用者转变为Vue的掌控者。现在,是时候打开Vue源码,开始你的探索之旅了!

九、手撕实现 ref & computed Demo

在线html运行:uutool.cn/html/

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>原生JS实现Vue的ref 和 Computed</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      }
      body {
        background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
        color: #fff;
        min-height: 100vh;
        padding: 20px;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .container {
        max-width: 800px;
        width: 100%;
        background: rgba(0, 0, 0, 0.7);
        border-radius: 16px;
        padding: 30px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
        backdrop-filter: blur(10px);
      }
      header {
        text-align: center;
        margin-bottom: 30px;
      }
      h1 {
        font-size: 2.5rem;
        margin-bottom: 10px;
        color: #fdbb2d;
        text-shadow: 0 0 10px rgba(253, 187, 45, 0.5);
      }
      .subtitle {
        font-size: 1.2rem;
        opacity: 0.8;
      }
      .content {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 30px;
      }
      .panel {
        background: rgba(30, 30, 46, 0.8);
        border-radius: 12px;
        padding: 25px;
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
      }
      .panel-title {
        font-size: 1.5rem;
        margin-bottom: 20px;
        color: #fdbb2d;
        display: flex;
        align-items: center;
        gap: 10px;
      }
      .panel-title i {
        font-size: 1.8rem;
      }
      .form-group {
        margin-bottom: 20px;
      }
      label {
        display: block;
        margin-bottom: 8px;
        font-weight: 500;
      }
      input {
        width: 100%;
        padding: 12px;
        border-radius: 8px;
        border: 2px solid #4e4e7c;
        background: rgba(26, 26, 42, 0.7);
        color: white;
        font-size: 1.1rem;
        transition: all 0.3s;
      }
      input:focus {
        outline: none;
        border-color: #fdbb2d;
        box-shadow: 0 0 0 3px rgba(253, 187, 45, 0.3);
      }
      .result {
        background: rgba(26, 26, 42, 0.7);
        border-radius: 8px;
        padding: 20px;
        margin-top: 20px;
        border-left: 4px solid #fdbb2d;
      }
      .result-title {
        font-size: 1.1rem;
        margin-bottom: 10px;
        color: #fdbb2d;
      }
      .result-value {
        font-size: 1.8rem;
        font-weight: bold;
        text-align: center;
        padding: 10px;
      }
      .explanation {
        margin-top: 30px;
        padding-top: 20px;
        border-top: 1px solid rgba(255, 255, 255, 0.1);
      }
      h3 {
        font-size: 1.4rem;
        margin-bottom: 15px;
        color: #fdbb2d;
      }
      p {
        line-height: 1.6;
        margin-bottom: 15px;
      }
      .code-block {
        background: rgba(26, 26, 42, 0.9);
        border-radius: 8px;
        padding: 15px;
        font-family: monospace;
        font-size: 1rem;
        overflow-x: auto;
        margin: 15px 0;
        border-left: 4px solid #fdbb2d;
      }
      .highlight {
        color: #fdbb2d;
        font-weight: bold;
      }
      @media (max-width: 768px) {
        .content {
          grid-template-columns: 1fr;
        }
        h1 {
          font-size: 2rem;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>原生JavaScript实现Vue的Computed属性</h1>
        <p class="subtitle">响应式数据与依赖追踪的纯JavaScript实现</p>
      </header>

      <div class="content">
        <div class="panel">
          <h2 class="panel-title">计算属性演示</h2>

          <div class="form-group">
            <label for="price">商品单价</label>
            <input type="number" id="price" value="25" />
          </div>

          <div class="form-group">
            <label for="quantity">购买数量</label>
            <input type="number" id="quantity" value="4" />
          </div>

          <div class="form-group">
            <label for="discount">折扣 (%)</label>
            <input type="number" id="discount" value="10" />
          </div>

          <div class="result">
            <div class="result-title">总价(含税)</div>
            <div id="totalPrice" class="result-value">$99.00</div>
          </div>
        </div>

        <div class="panel">
          <h2 class="panel-title">实现原理</h2>

          <div class="explanation">
            <h3>核心概念</h3>
            <p>这个实现模拟了Vue的响应式系统:</p>

            <ul>
              <li><span class="highlight">响应式数据</span> - 通过getter/setter追踪数据访问</li>
              <li><span class="highlight">依赖收集</span> - 自动追踪计算属性所依赖的数据</li>
              <li><span class="highlight">计算缓存</span> - 只有依赖变化时才重新计算</li>
              <li><span class="highlight">自动更新</span> - 依赖变化时自动更新计算结果</li>
            </ul>

            <h3>关键代码</h3>
            <p>创建响应式数据:</p>
            <div class="code-block">
              function reactive(obj) {
                return new Proxy(obj, {
                  get(target, key) {
                    // 依赖收集
                    track(target, key)
                    return target[key]
                  },
                  set(target, key, value) {
                    target[key] = value // 触发更新 trigger(target, key); return true;
                  },
                })
              }
            </div>

            <p>创建计算属性:</p>
            <div class="code-block">
                let value
                let dirty = true
                const effect = () => {
                  dirty = true
                }
                return {
                  get value() {
                    if (dirty) {
                      activeEffect = effect
                      value = getter()
                      dirty = false
                      activeEffect = null
                    }
                    return value
                  },
                }
              }
            </div>
          </div>
        </div>
      </div>
    </div>

    <script>
      // 全局变量用于依赖追踪
      let activeEffect = null
      const targetMap = new WeakMap()

      // 依赖收集
      function track(target, key) {
        if (!activeEffect) return

        let depsMap = targetMap.get(target)
        if (!depsMap) {
          depsMap = new Map()
          targetMap.set(target, depsMap)
        }

        let dep = depsMap.get(key)
        if (!dep) {
          dep = new Set()
          depsMap.set(key, dep)
        }

        dep.add(activeEffect)
      }

      // 触发更新
      function trigger(target, key) {
        const depsMap = targetMap.get(target)
        if (!depsMap) return

        const dep = depsMap.get(key)
        if (dep) {
          dep.forEach((effect) => effect())
        }
      }

      // 创建响应式对象
      function reactive(obj) {
        return new Proxy(obj, {
          get(target, key) {
            track(target, key)
            return target[key]
          },
          set(target, key, value) {
            target[key] = value
            trigger(target, key) // 触发订阅更新
            return true
          },
        })
      }

      // 创建计算属性
      function computed(getter) {
        let value
        let dirty = true

        const effect = () => {
          dirty = true
        }

        return {
          get value() {
            if (dirty) {
              // 设置当前活动effect
              activeEffect = effect
              // 计算新值
              value = getter()
              dirty = false
              activeEffect = null
            }
            return value
          },
        }
      }

      // 初始化应用
      document.addEventListener('DOMContentLoaded', () => {
        // 创建响应式对象
        const state = reactive({
          price: 25,
          quantity: 4,
          discount: 10,
        })

        // 创建计算属性
        const total = computed(() => {
          const subtotal = state.price * state.quantity
          const discountAmount = subtotal * (state.discount / 100)
          const totalAfterDiscount = subtotal - discountAmount
          // 添加10%的税
          return totalAfterDiscount * 1.1
        })

        // 绑定输入事件
        document.getElementById('price').addEventListener('input', (e) => {
          state.price = Number(e.target.value)
          updateUI()
        })

        document.getElementById('quantity').addEventListener('input', (e) => {
          state.quantity = Number(e.target.value)
          updateUI()
        })

        document.getElementById('discount').addEventListener('input', (e) => {
          state.discount = Number(e.target.value)
          updateUI()
        })

        // 更新UI
        function updateUI() {
          document.getElementById('price').value = state.price
          document.getElementById('quantity').value = state.quantity
          document.getElementById('discount').value = state.discount
          // 访问计算属性会触发计算(如果dirty)
          document.getElementById('totalPrice').textContent = `$${total.value.toFixed(2)}`
        }

        // 初始渲染
        updateUI()
      })
    </script>
  </body>
</html>