Vue响应式原理

什么是响应式

一种声明式模式:当数据变化时,依赖数据的逻辑自动执行更新,无需我们​​手动干预进行更新

为什么需要响应式

  • 前端 UI 与状态需要频繁同步:数据变化 → 函数重新执行 → 视图更新
  • 减少手动 DOM 操作:数据逻辑统一管理,避免手动触发UI变化
  • 避免忘记更新、状态错乱问题

Vue2 vs Vue3

  • Object.defineProperty:是个静态方法,会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象

  • Proxy:Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截

    • 基本操作(内置操作)指的是对对象属性和元信息(原型、可扩展性)进行访问或修改的标准行为,作为js对象模型的基础行为接口,不是js代码直接调用,但它们决定了对象的基本行为

      • \[Get\]\]→ obj.key

      • \[HasProperty\]\] →'key' in obj

      • \[OwnPropertyKeys\]\] → Object.getOwnPropertyNames()

      • \[DefineOwnProperty\]\] → Object.defineProperty()

特性 ​​Vue2 ​​Vue3
实现原理 Object.defineProperty递归给对象的每个属性添加getter/setter,进行依赖收集和派发更新 利用 ES6 的Proxy代理整个对象,拦截对对象属性的基本操作,结合effect、track和trigger实现依赖收集与触发
​​依赖收集 访问响应式数据时,通过getter进行依赖收集(收集当前正在执行的 ​​Watcher) 在effect执行时通过effect(fn)包裹,访问响应式数据时自动调用track(target, key)收集依赖
派发更新 修改数据时通过setter通知对应的 ​​Watcher​​ 执行更新(如:重新渲染组件) 修改响应式数据时,通过trigger(target, key)找到所有依赖该数据的effect,并重新执行这些方法
​​核心 API 依赖收集和更新逻辑散布在Watcher和Dep类中,较难复用 响应式核心函数:1.reactive()/ref()2.effect(fn)3.track(target, key)4.trigger(target, key)

Vue2限制及vue3改进

问题 原因 vue2解决 vue3改进
无法劫持新增/删除属性 ​​Object.defineProperty在初始化时遍历对象的所有属性,逐个用 getter/setter劫持动态添加新属性(如 obj.newKey = value),或删除属性(如 delete obj.key),不会被 defineProperty劫持 ​​Vue.set(object, key, value)​​ 和 ​​Vue.delete(object, key)​​ 这两个方法会手动为新属性添加响应式劫持 Vue 3 用 Proxy代理整个对象​​​​Proxy 可以拦截对象的任意操作,包括:读取、设置、删除、新增属性等
无法监听数组索引/长度变化 通过数组索引修改值(如 arr[0] = newValue)和修改 length不会被 Object.defineProperty拦截。因为js的限制,​​不能直接对数组索引使用 getter/setter去劫持​​(性能极差,也不可行) Vue 2 ​​重写了数组的7种常用方法:push()pop()shift()unshift()splice()sort()reverse() Vue 3 使用 ​​Proxy代理数组​​,可以拦截:数组索引的读写(如 arr[0])数组方法调用数组长度的变化(如 arr.length = 0)
无法原生支持 Map / Set 等集合类型 ​​Map、Set、WeakMap、WeakSet等 ES6 集合类型没有"属性"的概念,所以无法像普通对象那样用 getter/setter去劫持它们的变化 - Vue 3 使用 ​​Proxy​​,可以拦截 ​​集合类型的方法调用​​,比如:map.set(key, value)set.add(value)map.delete(key)set.delete(value)​
依赖收集和派发更新的逻辑散落在 Dep 和 Watcher 类中 响应式的核心逻辑是由两个主要类管理:​​Dep(Dependency)​​:负责收集依赖​​Watcher​​:代表一个"观察者",比如组件渲染函数、用户定义的 watch 等,它依赖某些数据,并在数据变化时执行回调这两个类在整个响应式流程中紧密耦合,逻辑分散,代码维护和理解成本较高数据变化时通过 Dep.notify()通知所有的 Watcher,流程相对隐晦,不利于扩展和调试 - Vue 3 ​​重构了响应式系统,使用函数式、组合式的设计思想​​,核心是:​​reactive()/ ref()​​:创建响应式数据​​effect()​​:注册副作用函数(类似 Vue 2 的 Watcher,但是更轻量和灵活)​​track()和 trigger()​​:分别负责依赖收集和触发更新,逻辑清晰分离整个响应式流程更加透明、模块化,基于 ​​Proxy + 函数组合​​,而不是基于复杂的类继承和事件通知机制

vue2响应式系统

  1. 初始化:使用Object.defineProperty为每个属性添加 getter 和 setter。让data中的属性都变成响应式数据,方便vue拦截对属性的访问和修改操作
  2. 依赖收集:组建渲染过程中访问到某个响应式数据时,会触发该属性的getter,这样当前正在运行的watcher就会被记录下来,作为该属性的一个依赖,后序数据变化时,vue会通知这个方法重新执行
  3. 派发更新:当我们修改某个响应式数据时(this.msg='vue2'),触发该属性的setter,vue会通知所有依赖该属性的watcher重新执行(重新渲染视图)

核心模块

模块 作用
​​Dep​​ 每个响应式属性都有一个 Dep,用于管理 Watcher 订阅者,数据变化时通知它们
​​Watcher​​ 代表一个依赖关系(比如某个组件渲染时用到了 data.name),在初始化时触发 getter 进行依赖收集,在数据更新时执行回调(比如更新视图)
​​Observer​​ 遍历 data 对象,通过Object.defineProperty给每个属性添加 getter/setter,实现数据劫持

核心实现

Dep: 依赖收集器,每个响应式属性都有一个 Dep 实例

js 复制代码
class Dep {
    constructor() {
         // 存放 Watcher 订阅者
        this.subs = [];
    }
    // 添加订阅者
    addSub(sub) {
        this.subs.push(sub);
    }
    // 移除订阅者
    removeSub(sub) {
        const index = this.subs.indexOf(sub);
        if (index > -1) this.subs.splice(index, 1);
    }
    // 通知所有订阅者更新
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}

Watcher: 观察者,代表一个依赖(比如模板中的某个数据绑定)

js 复制代码
// 全局变量,用于在 getter 中获取当前正在执行的 Watcher
Dep.target = null;
class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm; // vue实例,表示vue的ViewModel,持有数据、当发、计算属性等
        this.exp = exp; // 数据属性,比如 'name'
        this.cb = cb;   // 回调函数,数据变化时触发,比如更新视图
        this.value = this.get(); // 触发 getter,收集依赖
    }
    get() {
        Dep.target = this; // 将当前 watcher 设置为全局 target
        const value = this.vm.data[this.exp]; // 触发 getter,进行依赖收集
        Dep.target = null; // 收集完毕,清空全局依赖收集的上下文
        return value;
    }
    update() {
        const newValue = this.vm.data[this.exp];
        const oldValue = this.value;
        if (newValue !== oldValue) {
            this.value = newValue;
            this.cb.call(this.vm, newValue, oldValue); // 执行回调,比如更新 DOM
        }
    }
}

Observer: 将 data 对象的属性转换为响应式

js 复制代码
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    });
}

defineReactive函数

js 复制代码
function defineReactive(obj, key, val) {
    const dep = new Dep(); // 每个属性都有一个 Dep 实例

    // 递归处理嵌套对象(简化处理)
    observe(val);

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 收集依赖:如果当前有正在运行的 Watcher,将其添加到 Dep 中(依赖全局变量Dep.target = null;的赋值修改)
            if (Dep.target) {
                dep.addSub(Dep.target);
            }
            return val;
        },
        set(newVal) {
            if (newVal === val) return;
            val = newVal;
            // 新值也可能是对象,需要递归劫持
            observe(newVal);
            // 通知所有订阅者更新
            dep.notify();
        }
    });
}

简易 Vue 类

js 复制代码
class MiniVue {
    constructor(options) {
        this.data = options.data || {};
        this.methods = options.methods || {};

        // 将 data 挂载到实例上(可选,模拟 Vue 的 this.xxx 访问)
        Object.keys(this.data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return this.data[key];
                },
                set(newVal) {
                    this.data[key] = newVal;
                }
            });
        });

        // 使 data 变成响应式
        observe(this.data);

        // 简易编译过程模拟:创建一个 Watcher 来模拟视图更新
        // 假设我们监听 'name' 属性变化,并在变化时调用回调模拟更新 DOM
        new Watcher(this, 'name', (newVal, oldVal) => {
            console.log(`name 变化了: ${oldVal} -> ${newVal}`);
            // 这里通常会触发虚拟 DOM diff 和真实 DOM 更新,这里仅打印日志模拟
        });

        // 模拟用户修改数据,触发响应式
        // setTimeout(() => {
        //   this.name = 'Vue 2';
        // }, 1000);
    }
}

使用示例

js 复制代码
// 使用上面定义的 MiniVue
const app = new MiniVue({
    data: {
        name: 'Hello'
    },
    methods: {}
});

// 手动修改 name,触发 setter -> dep.notify() -> watcher.update()
// 在控制台可以看到回调被触发,打印出变化
app.name = 'Vue 2 响应式'; // 输出:name 变化了: Hello -> Vue 2 响应式

vue3响应式系统

关键API

构件 作用
reactive(obj)/ref(value) 创建响应式对象/值底层通过Proxy代理对象,或者包装原始值作为响应式引用
effect(fn) 注册副作用函数(自动更新)调用effect(fn),Vue在执行fn的过程中,自动追踪它访问了哪些响应式数据(即依赖收集)
track(target,key) 收集依赖副作用函数执行期间,访问到某个响应式对象的属性时,通过proxy的get拦截器调用track函数记录当前正在运行的effect函数依赖于哪个对象的哪个属性,并且建立映射关系
trigger(target,key) 派发更新修改某个响应式对象的属性时,通过Proxy的set拦截器调用trigger函数找到所有依赖该target.key的副作用函数,然后重新执行他们(eg:组件重新渲染)

示例代码:

js 复制代码
const state = reactive({ count: 0 })

effect(() => {
  console.log('count is', state.count)
})

state.count++  // 自动触发 effect 执行

核心流程

核心实现

定义副作用函数

js 复制代码
let activeEffect = null; // 存储当前执行的方法
const targetMap = new WeakMap();

effect(fn):注册副作用

js 复制代码
function effect(fn) {
    activeEffect = fn;
    fn(); // 执行一次,触发 getter,建立依赖
    activeEffect = null;
}

响应式追踪依赖关系的数据结构是一个嵌套关系,示意图如下所示:

track():依赖收集

js 复制代码
function track(target, key) {
    // key 是响应式对象,如一个被reactive包裹的对象
    if (!activeEffect) {
        return;
    }
    // targetMap以响应式对象为key,存储该对象属性的依赖关系
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        // depsMap以对象的属性名为key,存储该属性对应的所有依赖函数集合
        depsMap = new Map();
        targetMap.set(target, depsMap);
    }

    let dep = depsMap.get(key);
    if (!dep) {
        // dep 存储依赖该属性的所有方法,set数据结构确保每个依赖函数都只被添加一次
        dep = new Set();
        depsMap.set(key, dep);
    }

    dep.add(activeEffect); // 依赖添加成功
}

trigger():触发更新

js 复制代码
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const dep = depsMap.get(key);
    // 取出依赖该属性的所有方法,并且依次执行
    dep && dep.forEach(effect => effect());
}

reactive():响应式对象创建

js 复制代码
function reactive(target) {
    return new Proxy(target, {
        get(obj, key) {
            track(obj, key); // 依赖追踪
            return obj[key];
        },
        set(obj, key, value) {
            obj[key] = value;
            trigger(obj, key); // 触发更新
            return true;
        }
    });
}

业务场景

案例:上传进度展示组件

网盘的上传任务列表中会有多个任务项,每一项的上传进度和状态都会动态变化。我们希望:

  • 每当某个任务的进度变化时,页面自动更新
  • 不需要手动操作 DOM 或查找组件来更新视图

传统(非响应式)写法:

js 复制代码
task.progress = 40
updateDom(task) // 手动调用方法更新 UI

问题:

  • 维护成本高
  • 忘记调用 UI 更新函数容易出错
  • 多任务之间耦合严重

响应式方式重构(Vue3)

js 复制代码
import {reactive, computed} from 'vue'

const taskList = reactive([
    {id: 1, name: 'A.mp4', uploaded: 10, total: 100},
    {id: 2, name: 'B.pdf', uploaded: 60, total: 100}
])

const taskProgress = computed(() =>
    taskList.map(task => ({
        id: task.id,
        name: task.name,
        percent: Math.floor((task.uploaded / task.total) * 100)
    }))
)

模板中自动响应更新:

html 复制代码
<ul>
  <li v-for="task in taskProgress" :key="task.id">
    {{ task.name }} - {{ task.percent }}%
  </li>
</ul>

数据变化时自动更新视图:

js 复制代码
// 模拟上传进度
setInterval(() => {
    taskList[0].uploaded += 5
}, 1000);

无需手动更新视图、也无需绑定事件或监听 DOM,页面会每秒自动更新上传进度条

总结

  • 响应式让数据驱动视图变得简单高效
  • Vue2 使用 defineProperty,Vue3 采用 Proxy,后者更强大
  • 本质上是自动化的观察者模式

推荐资料

Q&A

相关推荐
真上帝的左手2 分钟前
24. 前端-js框架-Vue
前端·javascript·vue.js
3Katrina12 分钟前
《Stitch的使用指南以及AI新开发模式杂谈》
前端
无羡仙14 分钟前
按下回车后,网页是怎么“跳”出来的?
前端·node.js
喝拿铁写前端14 分钟前
Vue 实战:构建灵活可维护的菜单系统
前端·vue.js·设计模式
ZzMemory17 分钟前
一套通关CSS选择器,玩转元素定位
前端·css·面试
圆心角20 分钟前
小米面挂了
前端·面试
我的小月月22 分钟前
Vue移动端"回到顶部"组件深度解析:拖拽、动画与性能优化实践
前端
拳打南山敬老院22 分钟前
从零构建一个插件系统(六)低代码场景的插件构建思考
javascript·架构
前端康师傅24 分钟前
你还在相信前端加密吗?前端密码加密安全指南
前端·安全
小白白一枚11128 分钟前
HTML5的新特性
前端·html·html5