什么是响应式
一种声明式模式:当数据变化时,依赖数据的逻辑自动执行更新,无需我们手动干预进行更新
为什么需要响应式
- 前端 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响应式系统

- 初始化:使用
Object.defineProperty
为每个属性添加 getter 和 setter。让data中的属性都变成响应式数据,方便vue拦截对属性的访问和修改操作 - 依赖收集:组建渲染过程中访问到某个响应式数据时,会触发该属性的getter,这样当前正在运行的watcher就会被记录下来,作为该属性的一个依赖,后序数据变化时,vue会通知这个方法重新执行
- 派发更新:当我们修改某个响应式数据时(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,后者更强大
- 本质上是自动化的观察者模式
推荐资料
- Vue 官方文档:vuejs.org/
- 响应式深入讲解:vuejs.org/guide/extra...
- Object.defineProperty MDN 文档:developer.mozilla.org/zh-CN/docs/...
- Proxy MDN 文档:developer.mozilla.org/en-US/docs/...