响应式系统总结:从零到完整的闭环

经过前面几篇文章的深入探索,我们从最底层的 effect 开始,逐步构建起了 Vue3 响应式系统的完整图景。今天,我们将把所有组件串联起来,形成一个完整的闭环,并通过实战和性能分析,深入理解 Vue3 响应式系统的设计精髓。

前言:响应式系统的全景图

在开始整合之前,让我们先回顾一下整个响应式系统的架构: Vue3 中的响应式系统的核心思想可以概括为:在读取时收集依赖,在修改时触发更新。

串联所有组件:完整的响应式系统

1. 基础工具函数

javascript 复制代码
// ============ 工具函数 ============
function isObject(value) {
    return value !== null && typeof value === 'object';
}

function isFunction(value) {
    return typeof value === 'function';
}

function isArray(value) {
    return Array.isArray(value);
}

function isRef(r) {
    return !!(r && r.__v_isRef === true);
}

function isReactive(value) {
    return !!(value && value.__v_isReactive === true);
}

function isArrayIndex(key) {
    if (typeof key !== 'string') return false;
    const keyAsNumber = Number(key);
    return Number.isInteger(keyAsNumber) &&
           keyAsNumber >= 0 &&
           keyAsNumber < Number.MAX_SAFE_INTEGER;
}

2. 依赖管理核心

javascript 复制代码
// ============ 依赖管理 ============
const targetMap = new WeakMap();
let activeEffect = null;

// 操作类型枚举
const TrackOpTypes = {
    GET: 'get',
    HAS: 'has',
    ITERATE: 'iterate'
};

const TriggerOpTypes = {
    SET: 'set',
    ADD: 'add',
    DELETE: 'delete',
    CLEAR: 'clear'
};

// 特殊标识
const ITERATE_KEY = Symbol('iterate');

class ReactiveEffect {
    constructor(fn, scheduler = null) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.deps = [];
        this.active = true;
        this.runDepth = 0;
    }
    
    run() {
        if (!this.active) {
            return this.fn();
        }
        
        try {
            this.runDepth++;
            if (this.runDepth > 1000) {
                console.warn('检测到可能的无限循环');
                return;
            }
            
            activeEffect = this;
            return this.fn();
        } finally {
            this.runDepth--;
            activeEffect = null;
        }
    }
    
    stop() {
        if (this.active) {
            this.active = false;
            this.deps.forEach(dep => dep.delete(this));
            this.deps.length = 0;
        }
    }
}

function track(target, type, key) {
    if (!activeEffect) return;
    
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    
    // 处理迭代操作
    let depKey = key;
    if (type === TrackOpTypes.ITERATE) {
        depKey = ITERATE_KEY;
    }
    
    let dep = depsMap.get(depKey);
    if (!dep) {
        depsMap.set(depKey, (dep = new Set()));
    }
    
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
    }
}

function trigger(target, type, key, newValue, oldValue) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    
    const effectsToRun = new Set();
    
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => {
                if (effect !== activeEffect) {
                    effectsToRun.add(effect);
                }
            });
        }
    };
    
    // 处理普通 key
    if (key !== undefined) {
        add(depsMap.get(key));
    }
    
    // 处理数组特殊情况
    if (Array.isArray(target)) {
        if (key === 'length') {
            // length 变化需要触发所有索引 >= 新值的依赖
            const newLength = Number(newValue);
            depsMap.forEach((dep, key) => {
                if (isArrayIndex(key) && Number(key) >= newLength) {
                    add(dep);
                }
            });
        } else if (type === TriggerOpTypes.ADD && isArrayIndex(key)) {
            // 添加数组元素触发 length
            add(depsMap.get('length'));
        }
    } else {
        // 处理迭代操作
        if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
            add(depsMap.get(ITERATE_KEY));
        }
    }
    
    // 执行 effects
    effectsToRun.forEach(effect => {
        if (effect.scheduler) {
            effect.scheduler();
        } else {
            effect.run();
        }
    });
}

function effect(fn) {
    const _effect = new ReactiveEffect(fn);
    _effect.run();
    
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner;
}

3. Reactive 实现

javascript 复制代码
// ============ reactive ============
const reactiveHandlers = {
    get(target, key, receiver) {
        // 内部标记
        if (key === '__v_isReactive') return true;
        
        const value = Reflect.get(target, key, receiver);
        
        // 依赖收集
        track(target, TrackOpTypes.GET, key);
        
        // 嵌套响应式
        if (isObject(value)) {
            return reactive(value);
        }
        
        return value;
    },
    
    set(target, key, value, receiver) {
        const oldValue = target[key];
        const hadKey = target.hasOwnProperty(key);
        const oldLength = Array.isArray(target) ? target.length : undefined;
        
        const result = Reflect.set(target, key, value, receiver);
        
        if (!hadKey) {
            // 新增属性
            trigger(target, TriggerOpTypes.ADD, key, value);
        } else if (oldValue !== value) {
            // 修改属性
            trigger(target, TriggerOpTypes.SET, key, value, oldValue);
        }
        
        // 数组 length 隐式变化
        if (Array.isArray(target) && oldLength !== target.length) {
            trigger(target, TriggerOpTypes.SET, 'length', target.length);
        }
        
        return result;
    },
    
    deleteProperty(target, key) {
        const hadKey = target.hasOwnProperty(key);
        const oldValue = target[key];
        
        const result = Reflect.deleteProperty(target, key);
        
        if (result && hadKey) {
            trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
        }
        
        return result;
    },
    
    has(target, key) {
        track(target, TrackOpTypes.HAS, key);
        return Reflect.has(target, key);
    },
    
    ownKeys(target) {
        track(target, TrackOpTypes.ITERATE, ITERATE_KEY);
        return Reflect.ownKeys(target);
    }
};

function reactive(target) {
    if (!isObject(target)) return target;
    if (target.__v_isReactive) return target;
    
    return new Proxy(target, reactiveHandlers);
}

4. Ref 实现

javascript 复制代码
// ============ ref ============
class RefImpl {
    constructor(value, isShallow = false) {
        this._rawValue = value;
        this._value = isShallow ? value : toReactive(value);
        this.__v_isRef = true;
        this._isShallow = isShallow;
    }
    
    get value() {
        track(this, TrackOpTypes.GET, 'value');
        return this._value;
    }
    
    set value(newValue) {
        if (newValue !== this._rawValue) {
            this._rawValue = newValue;
            this._value = this._isShallow ? newValue : toReactive(newValue);
            trigger(this, TriggerOpTypes.SET, 'value', newValue);
        }
    }
}

function toReactive(value) {
    return isObject(value) ? reactive(value) : value;
}

function ref(value) {
    if (isRef(value)) return value;
    return new RefImpl(value);
}

function shallowRef(value) {
    return new RefImpl(value, true);
}

function isRef(r) {
    return !!(r && r.__v_isRef === true);
}

function unref(ref) {
    return isRef(ref) ? ref.value : ref;
}

function toReactive(value) {
    return isObject(value) ? reactive(value) : value;
}

class ObjectRefImpl {
    constructor(_object, _key) {
        this._object = _object;
        this._key = _key;
        this.__v_isRef = true;
    }
    
    get value() {
        return this._object[this._key];
    }
    
    set value(newValue) {
        this._object[this._key] = newValue;
    }
}

function toRef(object, key) {
    return new ObjectRefImpl(object, key);
}

function toRefs(object) {
    const result = {};
    for (const key in object) {
        if (object.hasOwnProperty(key)) {
            result[key] = toRef(object, key);
        }
    }
    return result;
}

5. Computed 实现

javascript 复制代码
// ============ computed ============
class ComputedRefImpl {
    constructor(getter, setter) {
        this.getter = getter;
        this.setter = setter;
        this._dirty = true;
        this._value = undefined;
        this.__v_isRef = true;
        
        this.effect = new ReactiveEffect(getter, () => {
            if (!this._dirty) {
                this._dirty = true;
                trigger(this, TriggerOpTypes.SET, 'value');
            }
        });
    }
    
    get value() {
        track(this, TrackOpTypes.GET, 'value');
        
        if (this._dirty) {
            this._dirty = false;
            this._value = this.effect.run();
        }
        
        return this._value;
    }
    
    set value(newValue) {
        if (this.setter) {
            this.setter(newValue);
        } else {
            console.warn('计算属性是只读的');
        }
    }
}

function computed(getterOrOptions) {
    let getter, setter;
    
    if (isFunction(getterOrOptions)) {
        getter = getterOrOptions;
        setter = null;
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    
    return new ComputedRefImpl(getter, setter);
}

6. Watch 实现

javascript 复制代码
// ============ watch ============
function traverse(value, seen = new Set()) {
    if (!isObject(value) || seen.has(value)) return value;
    
    seen.add(value);
    
    if (Array.isArray(value)) {
        value.forEach(item => traverse(item, seen));
    } else if (value instanceof Map) {
        value.forEach((v, k) => {
            traverse(v, seen);
            traverse(k, seen);
        });
    } else if (value instanceof Set) {
        value.forEach(v => traverse(v, seen));
    } else {
        Object.keys(value).forEach(key => traverse(value[key], seen));
    }
    
    return value;
}

function watch(source, cb, options = {}) {
    let getter;
    
    if (isRef(source)) {
        getter = () => source.value;
    } else if (isReactive(source)) {
        getter = () => source;
        options.deep = options.deep ?? true;
    } else if (Array.isArray(source)) {
        getter = () => source.map(s => {
            if (isRef(s)) return s.value;
            if (isReactive(s)) return traverse(s);
            if (isFunction(s)) return s();
            return s;
        });
    } else if (isFunction(source)) {
        if (cb) {
            getter = source;
        } else {
            return watchEffect(source, options);
        }
    }
    
    if (options.deep) {
        const baseGetter = getter;
        getter = () => traverse(baseGetter());
    }
    
    let oldValue;
    let cleanup;
    
    function onInvalidate(fn) {
        cleanup = fn;
    }
    
    const scheduler = () => {
        if (cleanup) cleanup();
        
        const newValue = getter();
        
        if (newValue !== oldValue) {
            cb(newValue, oldValue, onInvalidate);
        }
        
        oldValue = newValue;
    };
    
    const _effect = new ReactiveEffect(getter, scheduler);
    
    if (options.immediate) {
        scheduler();
    } else {
        oldValue = _effect.run();
    }
    
    return () => {
        _effect.stop();
        if (cleanup) cleanup();
    };
}

function watchEffect(effect, options = {}) {
    const scheduler = () => {
        if (options.flush === 'post') {
            Promise.resolve().then(() => _effect.run());
        } else {
            _effect.run();
        }
    };
    
    const _effect = new ReactiveEffect(effect, scheduler);
    _effect.run();
    
    return () => {
        _effect.stop();
    };
}

常见面试题解析

面试题 1:Vue3 的响应式原理是什么?

核心原理:Proxy + 依赖收集:

  1. 通过 Proxy 代理对象的所有操作
  2. 在 get 中通过 track 收集依赖
  3. 在 set 中通过 trigger 触发更新
  4. 使用 WeakMap + Map + Set 三层结构存储依赖
  5. 通过 effect 管理系统中的副作用
text 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    1. Proxy代理对象                          │
│                                                             │
│   ┌──────────────┐         ┌──────────────────────┐         │
│   │   原始对象     │        │     Proxy代理         │         │
│   │  {            │  代理  │   get: track收集      │         │
│   │    count: 0,  │◄───────┤   set: trigger触发    │        │
│   │    name: 'vue'│        │   deleteProperty     │         │
│   │  }            │        │   has...             │         │
│   └──────────────┘         └──────────────────────┘         │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│             2. 依赖收集 (track)                              │
│                                                             │
│   get操作触发 ──→ track函数 ──→ 查找依赖存储                    │
│                                                             │
│   ┌──────────────────────────────────────────┐              │
│   │        3. 三层依赖存储结构                  │              │
│   │                                          │              │
│   │   WeakMap          Map          Set      │              │
│   │   ┌─────┐        ┌─────┐      ┌─────┐    │              │
│   │   │target│──────►│key1 │─────►│effect1│  │              │
│   │   └─────┘        ├─────┤      ├─────┤    │              │
│   │                  │key2 │─┐    │effect2│  │              │
│   │                  └─────┘ │    └─────┘    │              │
│   │                           └───►┌─────┐   │              │
│   │                                │effect3│ │              │
│   │                                └─────┘   │              │
│   └──────────────────────────────────────────┘              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│             4. 副作用管理 (effect)                            │
│                                                             │
│   ┌────────────────────────────────────────┐                │
│   │  effect(() => {                        │                │
│   │    console.log(obj.count)  // 依赖收集  │                │
│   │  })                                    │                │
│   └────────────────────────────────────────┘                │
│                                                             │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│   │  effect1 │    │  effect2 │    │  effect3 │              │
│   │ (更新UI) │     │(计算属性)│     │ (watch)  │              │
│   └──────────┘    └──────────┘    └──────────┘              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────┐
│             5. 触发更新 (trigger)                             │
│                                                              │
│   set操作触发 ──→ trigger函数 ──→ 从存储结构中查找依赖            │
│                                          │                   │
│                                          ▼                   │
│   ┌──────────────────────────────────────────────────┐       │
│   │           执行所有相关的副作用函数                   │       │
│   │                                                  │       │
│   │   obj.count = 1                                  │       │
│   │        │                                         │       │
│   │        ▼                                         │       │
│   │   触发更新 ──→ 执行effect1 ──→ 更新UI               │       │
│   │            └─→ 执行effect2 ──→ 重新计算            │       │
│   └──────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────┘

面试题 2:Vue2 的 Object.defineProperty 和 Vue3 的 Proxy 有什么区别?

  1. Proxy 可以监听新增/删除属性;defineProperty 不能
  2. Proxy 可以监听数组索引和 length;defineProperty 不行
  3. Proxy 需要递归代理;defineProperty 初始化递归
  4. Proxy 性能更好,拦截操作更丰富

面试题 3:为什么 ref 需要 .value 而 reactive 不需要?

因为 Proxy 无法代理原始值,对于原始值的代理需要通过 value 包裹成对象:

javascript 复制代码
let count = 0; // 原始值,无法代理
// ref 包装成对象
const countRef = {
    value: 0
};
// 现在可以对 countRef 进行代理

面试题 4:computed 和 watch 有什么区别?

对比维度 computed watch
概念 计算属性,基于依赖缓存的计算值 侦听器,执行副作用操作
缓存机制 有缓存,只有依赖变化时才重新计算 无缓存,每次监听到变化都执行
返回值 必须返回一个值,模板中可直接使用 通常不返回值,用于执行逻辑操作
依赖追踪 自动追踪响应式依赖 手动指定要侦听的数据源
执行时机 懒执行,只有访问时才重新计算 立即执行(可配置)或数据变化时执行
异步操作 不支持异步 支持异步操作
性能特点 适合衍生状态,避免重复计算 适合处理开销大的操作或异步逻辑
使用场景 1. 模板中复杂表达式 2. 依赖其他数据的衍生值 3. 需要缓存的场景 1. 数据变化时执行异步操作 2. 操作DOM 3. 执行开销大的操作
访问方式 作为属性访问:state.count 通过回调函数执行
深度监听 自动深度追踪依赖 需要手动配置 deep: true
立即执行 自动计算 需要配置 immediate: true

面试题 5:Vue3 的响应式系统如何避免循环依赖?

  1. activeEffect 守卫:
javascript 复制代码
function trigger(target, key) {
    const effects = depsMap.get(key);
    effects.forEach(effect => {
        // 跳过当前正在执行的 effect
        if (effect !== activeEffect) {
            effect.run();
        }
    });
}
  1. 使用 Set 避免重复收集
javascript 复制代码
dep.add(activeEffect); // 自动去重
  1. 递归深度限制
javascript 复制代码
class ReactiveEffect {
    run() {
        this.runDepth++;
        if (this.runDepth > 1000) {
            console.warn('检测到无限循环');
            return;
        }
        // ... 执行逻辑
        this.runDepth--;
    }
}

性能分析:Vue3 响应式比 Vue2 快在哪里?

1. 初始化性能对比

javascript 复制代码
// Vue2:递归遍历所有属性
function vue2Init(obj) {
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key);
    });
    return obj;
}

// Vue3:代理整个对象,懒递归
function vue3Init(obj) {
    return new Proxy(obj, handlers); // 不递归
    // 只有在访问嵌套对象时才递归转换
}

性能差异:

  • Vue2: O(n) 初始化时间,n 为所有属性数量
  • Vue3: O(1) 初始化时间,只创建代理

2. 内存占用对比

javascript 复制代码
// Vue2:为每个属性创建闭包
function defineReactive(obj, key) {
    let value = obj[key];
    const dep = new Dep(); // 每个属性一个 dep
    
    Object.defineProperty(obj, key, {
        get() {
            dep.depend(); // 闭包引用
            return value;
        },
        set(newVal) {
            dep.notify();
        }
    });
}

// Vue3:共享 handlers,使用 WeakMap 存储依赖
const targetMap = new WeakMap(); // 依赖统一存储
const handlers = {}; // 单例,不重复创建

性能差异:

  • Vue2: 每个属性都有独立的 getter/setter 和闭包
  • Vue3: 所有对象共享 handlers,依赖集中存储

3. 数组操作性能

javascript 复制代码
// Vue2:重写数组方法
const arrayMethods = ['push', 'pop', 'shift', 'unshift'];
arrayMethods.forEach(method => {
    const original = Array.prototype[method];
    Object.defineProperty(array, method, {
        value: function(...args) {
            const result = original.apply(this, args);
            // 额外触发更新
            return result;
        }
    });
});

// Vue3:Proxy 直接拦截
const arr = new Proxy([], {
    set(target, key, value) {
        target[key] = value;
        // 统一处理更新
        return true;
    }
});

性能差异:

  • Vue2: 需要拦截每个方法,有额外开销
  • Vue3: 统一通过 set 拦截,更高效

4. 编译时优化

Vue3 性能提升:

  • 静态节点只创建一次
  • 更新时只比较动态部分
  • 减少了不必要的 VNode 创建

5. 批量更新机制

javascript 复制代码
// Vue2:同步更新
state.count++;
state.name = '张三'; // 触发两次更新

// Vue3:异步批量更新
state.count++;
state.name = '张三';
// 只触发一次更新

性能差异:

  • Vue2: 多次同步更新导致多次渲染
  • Vue3: 批量处理,减少渲染次数

结语

经过多篇文章的深入探索,我们完成了 Vue3 响应式系统的完整学习。响应式系统的设计思想,不仅适用于 Vue,也为我们理解和构建响应式应用提供了宝贵的参考。从底层原理到上层 API,每一层都是精心设计的结果,共同构成了这个优雅而强大的系统。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
ssshooter1 小时前
看完就懂 useLayoutEffect
前端·react.js·面试
结网的兔子1 小时前
前端开发(前言)——html,css,JavaScript和vue关系
javascript·css·html
parade岁月1 小时前
DOM 里有 Tailwind class,为什么样式还是不生效?v4 闭环修复实战
前端·vue.js
ashuicoder1 小时前
vue文件自动生成路由会成为主流
前端·vue.js
白中白121381 小时前
Vue系列-4
前端·javascript·vue.js
Ai runner1 小时前
Show call stack in perfetto from json input
java·前端·json
晴殇i1 小时前
前端防调试攻防战:如何保护你的JavaScript代码不被“偷窥”?
前端·javascript·面试
谦虚的酷猫1 小时前
SpiderDemo部分题目分析
javascript·网络爬虫
清粥油条可乐炸鸡2 小时前
tailwind-variants基本使用
前端·css