Skip to content

Vue响应式系统与计算属性缓存:从原理到实现

Vue的响应式系统是其核心特性之一,它让开发者能够以"数据驱动" 的方式构建应用——当数据变化时,视图会自动更新。而计算属性的缓存机制则是响应式系统的重要优化,避免了不必要的重复计算。本文将深入底层,解析这两个机制的实现原理。

一、计算属性的缓存机制:如何做到"依赖不变则结果不变"

计算属性的缓存并非简单的"记忆化",而是与Vue的响应式系统深度耦合的设计。其核心逻辑是:* 只有当计算属性依赖的响应式数据发生变化时,才会重新计算;否则直接返回缓存的结果*。

1. 缓存机制的实现原理

计算属性的缓存依赖于两个关键角色:

  • 计算属性Watcher:专门用于跟踪计算属性的依赖和缓存状态
  • 脏值标记(dirty):标识计算属性是否需要重新计算

(1)初始化阶段:创建计算属性Watcher

当组件初始化计算属性时,Vue会创建一个特殊的Watcher实例(称为"计算属性Watcher"),并执行计算属性的getter函数:

javascript
// 伪代码:计算属性初始化
function createComputedGetter(key) {
    return function computedGetter() {
        // 获取计算属性对应的Watcher
        const watcher = this._computedWatchers[key];
        if (watcher.dirty) {
            // 如果是脏的,执行getter重新计算
            watcher.evaluate();
        }
        // 依赖收集:让当前渲染Watcher依赖计算属性Watcher
        if (Dep.target) {
            watcher.depend();
        }
        // 返回缓存的结果
        return watcher.value;
    };
}

计算属性Watcher有一个dirty属性(默认true),表示"需要重新计算"。首次访问时,dirtytrue,会执行getter并将结果缓存到 watcher.value,然后将dirty设为false

(2)依赖追踪:关联计算属性与源数据

计算属性执行getter时,会访问源数据(如this.price)。此时,源数据的getter会触发依赖收集,将计算属性Watcher加入源数据的依赖列表( Dep):

javascript
// 伪代码:源数据的getter(简化版)
function getter() {
    // Dep.target指向当前活跃的Watcher(此处为计算属性Watcher)
    if (Dep.target) {
        // 将计算属性Watcher加入当前数据的Dep
        dep.depend();
    }
    return value;
}

当源数据变化时,会通知其Dep中的所有Watcher(包括计算属性Watcher),此时计算属性Watcher会将自己的dirty设为true,标记为" 需要重新计算"。

(3)缓存命中:依赖不变时直接返回缓存

当再次访问计算属性时:

  • dirtyfalse(依赖未变),直接返回watcher.value(缓存)
  • dirtytrue(依赖已变),重新执行getter计算,并更新缓存

2. 缓存与方法的本质区别

方法每次调用都会执行,而计算属性的缓存基于依赖变化而非调用次数。例如:

vue

<template>
  <!-- 计算属性:依赖不变时只执行1次 -->
  <p>{{ fullName }}</p>
  <!-- 方法:每次渲染都执行 -->
  <p>{{ getFullName() }}</p>
</template>

即使模板重新渲染(如其他不相关数据变化),只要计算属性的依赖没变,就会直接返回缓存值,这是计算属性性能优势的核心原因。

二、Vue响应式系统:数据劫持与依赖收集的协同工作

Vue的响应式系统核心目标是:当数据变化时,自动触发依赖该数据的代码(如视图渲染、计算属性、watch回调)执行。这个过程通过" 数据劫持"和"依赖收集"实现。

1. 核心设计思想

响应式系统的设计遵循"观察者模式",但做了针对性优化:

  • 数据劫持:拦截数据的读写操作,在读取时收集依赖,在修改时通知依赖更新
  • 依赖收集:精确记录"哪些代码依赖了哪些数据",确保数据变化时只更新相关代码

2. 数据劫持:如何监听数据的读写

数据劫持是响应式系统的"感知层",负责检测数据的访问和修改。Vue2和Vue3采用了不同的实现方案。

(1)Vue2:基于Object.defineProperty

Vue2通过Object.defineProperty重写对象的gettersetter,实现对属性读写的拦截:

javascript
// Vue2响应式处理(简化版)
function defineReactive(obj, key, value) {
    // 递归处理嵌套对象
    observe(value);

    // 创建依赖容器(Dep)
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        get() {
            // 读取数据时:收集依赖
            if (Dep.target) {
                dep.depend(); // 将当前Watcher加入Dep
            }
            return value;
        },
        set(newValue) {
            if (newValue === value) return;
            value = newValue;
            observe(newValue); // 新值也需要响应式处理
            // 修改数据时:通知依赖更新
            dep.notify(); // 触发Dep中所有Watcher的更新
        }
    });
}

// 递归处理对象所有属性
function observe(obj) {
    if (typeof obj !== 'object' || obj === null) return;
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key]);
    });
}

局限性

  • 无法监听对象新增/删除的属性(需用Vue.set/Vue.delete
  • 无法监听数组通过索引/长度修改的变化(需重写数组方法如pushsplice

(2)Vue3:基于Proxy

Vue3改用Proxy实现数据劫持,解决了Vue2的诸多局限:

javascript
// Vue3响应式处理(简化版)
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            // 读取数据时:收集依赖
            track(target, key);
            // 递归处理嵌套对象(懒代理,访问时才处理)
            if (typeof result === 'object' && result !== null) {
                return reactive(result);
            }
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = Reflect.get(target, key, receiver);
            if (oldValue === value) return true;
            const result = Reflect.set(target, key, value, receiver);
            // 修改数据时:通知依赖更新
            trigger(target, key);
            return result;
        },
        deleteProperty(target, key) {
            const hadKey = Reflect.has(target, key);
            const result = Reflect.deleteProperty(target, key);
            if (hadKey && result) {
                // 删除属性时:通知更新
                trigger(target, key);
            }
            return result;
        }
    });
}

优势

  • 原生支持监听对象新增/删除属性
  • 原生支持监听数组索引、长度变化
  • 采用"懒代理"模式,只在访问嵌套对象时才递归处理,性能更优

3. 依赖收集:如何记录"谁依赖了数据"

依赖收集是响应式系统的"记忆层",负责记录"哪些代码依赖了哪些数据",核心角色是DepWatcher

(1)Dep:依赖容器

每个响应式数据(或属性)都对应一个Dep实例,用于存储依赖该数据的所有Watcher

javascript
// 伪代码:Dep类
class Dep {
    constructor() {
        this.subscribers = new Set(); // 存储依赖的Watcher(去重)
    }

    // 收集依赖:将Watcher加入容器
    depend() {
        if (Dep.target) {
            this.subscribers.add(Dep.target);
        }
    }

    // 通知更新:触发所有Watcher的更新
    notify() {
        this.subscribers.forEach(watcher => watcher.update());
    }
}

// Dep.target:当前活跃的Watcher(全局变量,同一时间只有一个)
Dep.target = null;

(2)Watcher:观察者

Watcher是"依赖代码"的封装(如模板渲染函数、计算属性、watch回调),当依赖的数据变化时,Watcher会执行对应的代码:

javascript
// 伪代码:Watcher类
class Watcher {
    constructor(fn, options) {
        this.fn = fn; // 需要执行的代码(如渲染函数)
        this.options = options;
        this.dirty = options.lazy || false; // 计算属性用:标记是否需要重新计算
        if (!options.lazy) {
            this.run(); // 非懒加载的Watcher(如渲染Watcher)初始化时执行一次
        }
    }

    // 执行依赖代码
    run() {
        Dep.target = this; // 将当前Watcher设为活跃
        this.value = this.fn(); // 执行代码(会触发数据getter,收集依赖)
        Dep.target = null; // 重置活跃Watcher
    }

    // 标记为脏(计算属性用)
    update() {
        if (this.options.lazy) {
            this.dirty = true; // 计算属性:只标记脏,不立即执行
        } else {
            this.run(); // 普通Watcher:立即执行
        }
    }

    // 计算属性重新计算
    evaluate() {
        this.run();
        this.dirty = false; // 计算后标记为干净
    }
}

(3)依赖收集的完整流程

以模板渲染为例,依赖收集的过程如下:

  1. 初始化渲染Watcher:组件挂载时,创建渲染Watcher,其fn为组件的渲染函数
  2. 执行渲染函数Watcher.run()被调用,Dep.target设为当前Watcher
  3. 访问数据触发getter:渲染函数中访问this.message,触发messagegetter
  4. 收集依赖messageDep调用depend(),将当前渲染Watcher加入subscribers
  5. 数据变化触发setter:当this.message = 'new'时,触发messagesetter
  6. 通知更新messageDep调用notify(),所有依赖的Watcher(如渲染Watcher)执行update()
  7. 重新渲染:渲染Watcher的run()执行,重新调用渲染函数,视图更新

4. 不同类型的Watcher

响应式系统中存在多种Watcher,各自承担不同职责:

  • 渲染Watcher:每个组件一个,负责执行组件渲染函数
  • 计算属性Watcher:每个计算属性一个,负责跟踪依赖并缓存结果(lazy: true
  • 用户Watcher:由watch选项或watchAPI创建,负责执行用户定义的回调

三、响应式系统的局限性及解决方法

尽管Vue的响应式系统已经很强大,但仍存在一些边缘情况需要特殊处理。

1. Vue2的局限性及解决方案

  • 对象新增/删除属性不响应
    原因:Object.defineProperty只能拦截已有属性
    解决:用Vue.set(obj, key, value)this.$set新增属性;用Vue.delete删除属性

  • 数组索引/长度修改不响应
    原因:Object.defineProperty难以高效监听数组索引变化
    解决:Vue2重写了数组的7个方法(pushpopsplice等),调用这些方法会触发更新;或用Vue.set(arr, index, value)

  • 嵌套对象需递归处理
    原因:defineReactive需提前递归处理所有嵌套属性,性能损耗
    解决:Vue3改用Proxy的"懒代理"模式优化

2. Vue3的局限性及解决方案

  • Proxy无法监听原始值(如字符串、数字)
    原因:Proxy只能代理对象,不能代理原始值
    解决:用ref包装原始值(ref(0)),ref内部通过对象的value属性实现响应式

  • 某些特殊对象代理受限
    原因:Proxy对MapSet等内置对象的代理需要特殊处理
    解决:Vue3提供shallowRefcustomRef等API,支持自定义响应式逻辑

  • IE浏览器不支持Proxy
    原因:Proxy是ES6特性,IE完全不支持
    解决:如需兼容IE,需使用Vue2或通过Babel+polyfill(效果有限)

四、总结

Vue的响应式系统是"数据驱动"理念的完美体现,其核心是:

  • 数据劫持:Vue2用Object.defineProperty,Vue3用Proxy,拦截数据读写
  • 依赖收集:通过Dep(依赖容器)和Watcher(观察者)记录数据与依赖代码的关系
  • 自动更新:数据变化时,Dep通知所有Watcher执行更新逻辑

而计算属性的缓存机制则是响应式系统的精细化优化:通过dirty标记和依赖追踪,确保只有当源数据变化时才重新计算,大幅提升性能。

理解这些底层原理,不仅能帮助我们写出更高效的Vue代码,还能让我们在遇到响应式相关问题时(如数据更新但视图不刷新),快速定位并解决问题。