当前位置: 首页 > news >正文

vue2和vue3的响应式原理

1. 数据响应式

  • 核心:建立响应式数据副作用函数之间的依赖关系
  • 依赖收集: 当在副作用函数中访问响应式数据的属性时,Vue 会自动记录该数据与函数的依赖关系。
  • 触发更新:当这些响应式数据发生变化时,所有依赖它们的副作用函数都会重新执行。
  • 副作用函数:会读取响应式数据,并在数据变化时需要重新执行的函数。Vue 会自动追踪它们的依赖关系。这类函数通常会对外部环境产生影响(如修改 DOM、打印日志等),因此被称为 “副作用”。

1.1 响应式数据

  • Vue 2 中使用 Object.defineProperty 实现的 data 对象
  • Vue 3 中使用 Proxy 实现的 reactive/ref 对象

1.2 Vue 的响应式系统 只能在特定时机(副作用函数执行时) 收集依赖:

在这里插入图片描述

  • Vue 2:通过 Object.definePropertygetter拦截属性访问。
  • Vue 3:通过 Proxyget 拦截属性访问。

关键点:

  • 只有当 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
});
  • 在组件的 setupmethods 中使用
    • Vue 的模板(<template>)最终会被编译成一个 渲染函数(render function),而 渲染函数本身就是一个副作用函数。
      // 你的模板:
      <template><div>{{ sum() }}</div></template>// 会被编译成类似这样的渲染函数:
      function render() {return h('div', sum()); // h() 是 createElement 的简写
      }
      
    • 无论是 methods 还是 setup 返回的函数,只要在模板中被使用,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” 负责重新计算。

总结

  1. 实现核心
    基于 Object.defineProperty 劫持对象属性的 getter 和 setter,结合 Dep(依赖管理器)和 Watcher(观察者)实现依赖收集和更新触发。
  2. 关键流程
    劫持数据:初始化时递归遍历 data 中的所有属性,用 Object.defineProperty 重写 getter 和 setter。
    getter:当属性被访问时,触发依赖收集(让 Dep 记录当前使用该属性的 Watcher)。
    setter:当属性被修改时,触发更新(让 Dep 通知所有记录的 Watcher 执行更新)。
    依赖管理:
    Dep:每个属性对应一个 Dep 实例,负责存储依赖该属性的 Watcher(类似 “通讯录”)。
    Watcher:组件渲染、watch、computed 都会对应一个 Watcher,负责在数据变化时执行具体更新(如重新渲染、执行回调)。
  3. 局限性
    只能劫持已有属性,新增 / 删除属性需手动调用 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;
}

总结

  1. 实现核心
    基于 ES6 的 Proxy 代理整个对象,配合 effect(副作用函数)实现更全面的响应式。
  2. 关键流程
  • 劫持数据:用 Proxy 直接代理原始对象,拦截所有操作(包括读写、新增、删除、数组索引修改等)。
    • get 拦截器:属性被访问时,触发 track 函数收集依赖(记录当前 effect)。
    • set 拦截器:属性被修改时,触发 trigger 函数执行所有依赖的 effect。
  • 依赖管理:
    • 用 targetMap(WeakMap)存储依赖关系,结构为:target → depsMap(属性→依赖集合)→ dep(Set存储effect)。
    • effect 函数:封装副作用逻辑(如渲染、回调),执行时通过 activeEffect 标记当前依赖,自动关联数据。
  1. 优势
  • 拦截范围更广:天然支持新增 / 删除属性、数组索引修改,无需手动处理。
  • 懒处理深层对象:访问深层属性时才递归代理,初始化性能更优。
  • 更简洁的依赖管理:用 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')
http://www.lryc.cn/news/586134.html

相关文章:

  • Java中的内存溢出详解
  • 【Python练习】039. 编写一个函数,反转一个单链表
  • Linux系统使用Verdaccio搭建Npm私服
  • 初学者关于算法复杂度的学习笔记
  • python数据分析及可视化课程介绍(01)以及统计学的应用、介绍、分类、基本概念及描述性统计
  • 【Datawhale AI 夏令营】 用AI做带货视频评论分析(二)
  • 使用Java完成下面程序
  • 13. https 是绝对安全的吗
  • Spring AOP 是如何生效的(入口源码级解析)?
  • 基于Java的Markdown到Word文档转换工具的实现
  • 码头智能哨兵:AI入侵检测系统如何终结废钢盗窃困局
  • DirectX Repair修复工具下载,.NET修复,DirectX修复
  • 贪心算法题解——跳跃游戏 II【LeetCode】
  • 电商订单数据分析全流程:从数据处理到可视化洞察
  • AI产品经理面试宝典第11天:传统软件流程解析与AI产品创新对比面试题与答法
  • 网络连接:拨号连接宽带PPPOE
  • 维基艺术图片: python + scrapy 爬取图片
  • 物联网设备数据驱动3D模型的智能分析与预测系统
  • 深入理解 QSettings:Qt 中的应用程序配置管理
  • 多线程的区别和联系
  • SQL server之版本的初认知
  • linux系统----LVS负载均衡集群(NET/DR)模式
  • docker-compose方式搭建lnmp环境——筑梦之路
  • 【LeetCode】算法详解#8 ---螺旋矩阵
  • .gitignore
  • JVM 类加载过程
  • 安全初级作业1
  • Docker-镜像构建原因
  • 十三、K8s自定义资源Operator
  • Java面试基础:面向对象(1)