vue2和vue3的响应式原理
1. 数据响应式
- 核心:建立
响应式数据
与副作用函数
之间的依赖关系 - 依赖收集: 当在副作用函数中访问
响应式数据的属性
时,Vue 会自动记录该数据与函数的依赖关系。 - 触发更新:当这些响应式数据发生变化时,所有依赖它们的副作用函数都会重新执行。
- 副作用函数:会读取响应式数据,并在数据变化时需要重新执行的函数。Vue 会自动追踪它们的依赖关系。这类函数通常会对外部环境产生影响(如修改 DOM、打印日志等),因此被称为 “副作用”。
1.1 响应式数据
- Vue 2 中使用
Object.defineProperty
实现的data
对象 - Vue 3 中使用
Proxy
实现的reactive/ref
对象
1.2 Vue 的响应式系统 只能在特定时机(副作用函数执行时) 收集依赖:
- Vue 2:通过
Object.defineProperty
的getter
拦截属性访问。 - Vue 3:通过
Proxy
的get
拦截属性访问。
关键点:
- 只有当 Vue 正在执行一个副作用函数(如 computed、watchEffect、render) 时,它才会记录哪些数据被访问了。
- 普通函数(如 sum())执行时,Vue 不会记录依赖,因此无法建立响应式关系。
1.3 在 Vue 中,以下情况会产生副作用函数:
- 组件的渲染函数(render)
render()
是 Vue 自动管理的副作用函数,它会自动收集所有依赖的响应式数据。- 当
render()
执行时,如果调用了sum(
),而 sum() 内部又访问了state.a
和state.b
,那么 Vue 就会记录这个依赖关系 - 依赖数据变化 → 触发 render 重新执行 → 重新调用 sum() → 更新视图。
当 render() 执行时,如果调用了 sum(),而 sum() 内部又访问了 state.a 和 state.b,那么 Vue 就会记录:
- watchEffect 的回调函数
- watch 的侦听器函数(第一个参数)
- computed 的计算函数(getter)
1.4 将普通函数变成副作用函数
- 用
computed
包装
const sum = computed(() => state.a + state.b);
- 用
watchEffect
监听
watchEffect(() => {console.log("sum:", state.a + state.b); // 自动追踪 state.a 和 state.b
});
- 在组件的
setup
或methods
中使用- Vue 的模板(
<template>
)最终会被编译成一个 渲染函数(render function
),而 渲染函数本身就是一个副作用函数。// 你的模板: <template><div>{{ sum() }}</div></template>// 会被编译成类似这样的渲染函数: function render() {return h('div', sum()); // h() 是 createElement 的简写 }
- 无论是 methods 还是 setup 返回的函数,只要
在模板中被使用
,Vue 都能追踪其内部访问的响应式数据, 数据变化时会触发重新渲染
- Vue 的模板(
<template><div>{{ sum() }}</div> <!-- 自动收集依赖 -->
</template><script>
export default {setup() {const state = reactive({ a: 1, b: 2 });const sum = () => state.a + state.b; // 由于在模板中使用,Vue 会追踪依赖return { sum };},
};
</script>
2. 数据劫持
读响应式对象
的某个属性
才会实现数据劫持,才能进行依赖收集,其本质是拦截数据的读写操作,并在操作发生时执行额外逻辑(如更新视图、记录依赖)
2.1 vue2数据劫持:Object.defineProperty
- Vue 2 使用
Object.defineProperty
对data
对象的每个属性进行拦截,将其转换为 getter/setter
: - getter:在属性被访问时触发,用于
依赖收集(
记录哪些组件或计算属性依赖该数据) - setter:在属性被修改时触发,用于
派发更新
(通知依赖该数据的部分重新渲染)
let data = { name: 'Vue' };Object.defineProperty(data, 'name', {get() {console.log('读取 name');return value;},set(newVal) {console.log('更新 name');value = newVal;// 触发视图更新}
});
这个添加的是函数使用的标志,通过这个标志来绑定依赖函数
function observe(obj) {for (const key in obj) {let internalValue = obj[key]; // 缓存原始值let funcs = []; // 存储依赖该属性的函数Object.defineProperty(obj, key, {get: function () {// 依赖收集:记录当前使用该属性的函数(通过 window.__func 标记)if (window.__func && !funcs.includes(window.__func)) {funcs.push(window.__func);}return internalValue;},set: function (val) {internalValue = val; // 更新缓存值// 触发更新:执行所有依赖该属性的函数for (var i = 0; i < funcs.length; i++) {funcs[i]();}},});}
}
2.2 Vue3 数据劫持: Proxy 代理
Vue 3 使用 Proxy 直接代理整个对象(而非像 Vue 2 那样递归劫持每个属性),可以拦截所有类型的操作(包括新增/删除属性、数组索引修改等)
const rawData = { count: 0 };
const proxy = new Proxy(rawData, {get(target, key, receiver) {track(target, key); // 依赖收集return Reflect.get(target, key, receiver);// 反射获取原始值},set(target, key, value, receiver) {const result = Reflect.set(target, key, value, receiver);trigger(target, key); // 触发更新return result;}
});
track函数
const targetMap = new WeakMap(); // 存储所有依赖关系function track(target, key) {if (!activeEffect) return; // 若无活跃的 effect,直接返回// 1. 获取对象的依赖映射表// 寻找对象的依赖关系let depsMap = targetMap.get(target);if (!depsMap) {targetMap.set(target, (depsMap = new Map()));}// 2. 获取属性的依赖集合// 查找特定对象的某个属性(key)对应的依赖集关系let dep = depsMap.get(key);if (!dep) {depsMap.set(key, (dep = new Set()));}// 3. 将当前 effect 添加到依赖集合中dep.add(activeEffect);
}
activeEffect
是一个全局变量,用于标记当前正在执行的 effect
effect
// 你的代码中,effect 函数的完整实现
let activeEffect = null;
function effect(fn) {const effectFn = () => {activeEffect = effectFn; // 标记当前依赖fn(); // 执行函数,触发数据读取(进而触发 track)activeEffect = null; // 清除标记};effectFn(); // 首次执行,收集依赖return effectFn;
}
trigger函数
function trigger(target, key) {const depsMap = targetMap.get(target);if (!depsMap) return;const dep = depsMap.get(key);if (dep) {// 执行所有依赖该属性的 effectdep.forEach(effect => effect());}
}
2.3 对比
3. Vue2的响应式原理
数据劫持解决了 “如何知道数据被读写”,而 Watcher(Vue2)、Dep(Vue2)或 effect(Vue3)则解决了 “知道之后该通知谁更新”。
- 数据劫持是 “感知变化的手段”,
- Watcher/Dep/effect 是 “管理依赖关系的机制”。
Vue 2 的响应式原理基于 Object.defineProperty 和发布-订阅模式,通过 getter/setter 劫持数据变化,结合 Dep 和 Watcher 实现依赖追踪和视图更新。
Dep和Watcher
类似 deps(存储依赖函数)的数组
// 你的代码中,用数组存储依赖函数
let funcs = []; // 存储依赖该属性的函数
- Dep:每个响应式属性对应一个 Dep 实例,本质是 “依赖容器”(类似你的 funcs 数组),负责收集和管理依赖它的 Watcher。
- Watcher:每个需要响应数据变化的 “角色”(组件渲染、computed、watch)都会对应一个 Watcher,
本质
是 “更新触发器”(类似你的 funcs 里的函数)。
两者关系
- 当属性被访问(getter),Dep 会 “记住” 当前活跃的 Watcher(通过 Dep.target 标记,类似你的 window.__func)。
- 当属性被修改(setter),Dep 会通知所有 “记住的” Watcher 执行更新。
Watcher 的核心作用:「谁需要响应式更新,谁就是 Watcher」
Watcher
Watcher 本质是一个 “更新执行者”,它封装了 “当数据变化时,需要执行的具体操作”。
比如:
- 模板渲染时,需要把数据显示到页面上 → 有一个 “渲染 Watcher” 负责重新渲染页面。
- 你写了 watch: { name() { … } } → 有一个 “用户自定义 Watcher” 负责执行这个回调。
- 计算属性 computed: { fullName() { … } } → 有一个 “计算属性 Watcher” 负责重新计算。
总结
- 实现核心
基于 Object.defineProperty 劫持对象属性的 getter 和 setter,结合 Dep(依赖管理器)和 Watcher(观察者)实现依赖收集和更新触发。 - 关键流程
劫持数据:初始化时递归遍历 data 中的所有属性,用 Object.defineProperty 重写 getter 和 setter。
getter:当属性被访问时,触发依赖收集(让 Dep 记录当前使用该属性的 Watcher)。
setter:当属性被修改时,触发更新(让 Dep 通知所有记录的 Watcher 执行更新)。
依赖管理:
Dep:每个属性对应一个 Dep 实例,负责存储依赖该属性的 Watcher(类似 “通讯录”)。
Watcher:组件渲染、watch、computed 都会对应一个 Watcher,负责在数据变化时执行具体更新(如重新渲染、执行回调)。 - 局限性
只能劫持已有属性,新增 / 删除属性需手动调用 Vue.set/Vue.delete。
对数组支持有限,需重写 push/pop 等方法才能监听变化。
深层对象需初始化时递归劫持,性能开销较大。
4. Vue3的响应式原理
Vue 3 的响应式系统基于 Proxy 和 依赖收集实现
Effect
Vue3 用 effect 替代了 Watcher,用 targetMap 替代了 Dep
- 你的 activeEffect 就是 Vue3 中标记 “当前谁在依赖数据” 的变量。
- 你的 targetMap(WeakMap)→ depsMap(Map)→ dep(Set)结构,就是 Vue3 中管理 “数据→依赖” 关系的完整方案。
// 你的代码中,effect 函数的完整实现
let activeEffect = null;
function effect(fn) {const effectFn = () => {activeEffect = effectFn; // 标记当前依赖fn(); // 执行函数,触发数据读取(进而触发 track)activeEffect = null; // 清除标记};effectFn(); // 首次执行,收集依赖return effectFn;
}
总结
- 实现核心
基于 ES6 的 Proxy 代理整个对象,配合 effect(副作用函数)实现更全面的响应式。 - 关键流程
- 劫持数据:用 Proxy 直接代理原始对象,拦截所有操作(包括读写、新增、删除、数组索引修改等)。
- get 拦截器:属性被访问时,触发 track 函数收集依赖(记录当前 effect)。
- set 拦截器:属性被修改时,触发 trigger 函数执行所有依赖的 effect。
- 依赖管理:
- 用 targetMap(WeakMap)存储依赖关系,结构为:target → depsMap(属性→依赖集合)→ dep(Set存储effect)。
- effect 函数:封装副作用逻辑(如渲染、回调),执行时通过 activeEffect 标记当前依赖,自动关联数据。
- 优势
- 拦截范围更广:天然支持新增 / 删除属性、数组索引修改,无需手动处理。
- 懒处理深层对象:访问深层属性时才递归代理,初始化性能更优。
- 更简洁的依赖管理:用 effect 和 targetMap 替代 Dep 和 Watcher,逻辑更清晰。
5. 常见的响应式丢失
案例1
这里的数据和函数是不能关联上的
let a;
function m(){}
答:数据必须在函数中被读到,才能进行关联
案例2
这里的数据和函数是不能关联上的
const a = reactive({name: 'zhangsan',age: 18
})
function m (){console.log(a)
}
答:数据本身是响应式对象
的某个属性
const a = reactive({name: 'zhangsan',age: 18
})
function m (){console.log(a.age)
}
案例3
当点击页面时count发生变化,但是double却没有发生变化
<template><div>得到传入的属性:{{ count }}</div><div>doubled: {{ doubleCount }}</div>
</template><script setup>
import { computed, ref, watch, watchEffect } from 'vue';const props = defineProps({count: Number,
});const doubleCount = ref(props.count * 2);
</script>
答:数据响应式是数据和函数的关联
,但这里是数据和数据的关联,在render的运行期间,读到的数据(template里的数据)都会和render关联起来
案例4
这里的数据发生变化,为什么
<template><div>得到传入的属性:{{ count }}</div><div>doubled: {{ doubleCount }}</div>
</template><script setup>
import { computed, ref, watch, watchEffect } from 'vue';const props = defineProps({count: Number,
});
const doubleCount = ref(0)
watchEffect(()=>{console.log('watchEffect')doubleCount.value= props.count*2
})
答:render 和props.count 、doubleCount.value关联,watchEffect和props.count关联,doubleCount.value(这里是写的数据,不是读到的数据)
案例5
这里的doubleCount没有发生变化
import { computed, ref, watch, watchEffect } from 'vue';const props = defineProps({count: Number,
});
function useDouble(count){const doubleCount = ref(count*2)watchEffect(()=>{console.log('watchEffect')doubleCount.value= count*2 })return doubleCount = useDouble(props.count)
}
答:封装的hook函数,必须读响应式对象的某个属性,应该写成
function useDouble(props){const doubleCount = ref(props.count*2)watchEffect(()=>{console.log('watchEffect')doubleCount.value= props.count*2 })return doubleCount
}
const doubleCount = useDouble(props)
案例5
没有关联上
let c = props.count
const doubleCount = computed(()=>c*2)
答: 必须读响应式对象的某个属性
let c = props.count
const doubleCount = computed(()=>props.count*2)
hook响应式封装使用,保持响应式
function useDouble(props,propName){const doubleCount = computed(()=>props[propName]*2)return doubleCount
}
const doubleCount = useDouble(props,'count')