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

理解 Vue 2 的响应式原理:数据劫持与依赖收集的背后

在Vue2中,响应式系统是一切魔法的源头,无论是模板中的数据绑定,还是computed,watch的精准监听,都离不开Vue背后的响应式机制,本文将从源码角度出发,结合实例,深入剖析vue2是如何通过数据劫持(Object.defineProperty)和依赖收集实现响应式的

一.Vue2响应式系统基本原理

vue2中响应式原理主要就是利用object.defineProperty劫持对象属性的读写操作.在读取时进行依赖收集,在写入时进行派发更新 

  1. 数据劫持: Vue2使用Object.defineProperty函数对组件的data对象属性进行劫持,当读取data中的属性时触发get,当修改data中的属性时触发set 
  2. 依赖收集: 当模版或者计算属性等引用了data中响应式数据时,Vue将这些消费者收集起来,建立数据与消费者之间的关联  
  3. 派发更新 当响应式数据变化时,通过dep来执行watcher的notify方法进行通知更新

1.1 数据劫持 Object.defineProperty  

Vu2 使用Object.defineProperty 函数对组件data对象的属性进行劫持

局限性: Object.defineProperty只能劫持对象的属性,因此Vue2无法自动侦测到对象属性的添加或是删除,以及直接通过索引修改数组项的情况,Vue解决这个问题的方式是提供了全局方法如Vue.set和Vue.delete, 以及修改数组时应该使用的一系列方法(如push,splice等 )
data() {return {user: { name: "Alice" },list: ['apple','banana']};
},
methods: {addAge() {this.user.age = 25; // ❌ 视图不更新this.$set(this.user, "age", 25); // ✅ 添加属性//数组this.list[1]='grape'; //视图不更新this.$set(this.list, 1, 'grape'); // ✅ 视图更新},deleteName() {delete this.user.name; // ❌ 视图不更新this.$delete(this.user, "name"); // ✅ 删除属性}

二. 响应式的实现关键类: Observer,Dep,Watcher,defineReactive

Vue的响应式系统中主要涉及一下三个核心类: 

  • Observer:  用于对对象进行递归响应式处理; 
  • Dep(依赖): 每个被劫持的属性都会对应一个Dep实例,用于收集依赖并在数据变更时通知更新; 
  • Watcher(观察者): 每个组件或计算属性,侦听器在初始化时会创建一个Watcher,用于响应数据变化 
  • defineReactive; 具体实现对属性的劫持,依赖收集 & 通知更新

他们之间的关系如下 

          data.x
            |
      defineReactive
            |
          getter <------ Dep.target = 当前 Watcher
            |
          Dep.depend()
            |
          Dep ←———— Watcher(视图更新逻辑)
            |
          setter
            |
         Dep.notify() —→ Watcher.update()
 

三. Observer( ): 让一个对象变成响应式

Vue会在初始化数据时调用observe( )方法,每个对象在挂载前,都先会被"响应化

function observe(value){//如果传入的不是对象或者null,就不做任何处理,直接返回     if(typeof value !== 'object' || value===null) return ;    //如果是,对其进行观察处理,并返回一个Observer实例return new Observe(value)
}

而Observer的核心逻辑就是对对象的每个属性进行递归处理

//Observer.js 
import { defineReactive } from './defineReactive.js';
import { Dep } from './dep.js';
import { arrayMethods, augmentArray } from './arrayMethods.js';
import { observe } from './observe.js';class Observer {constructor(value) {//要被观察的数据对象或数组this.value = value;//实例化一个依赖收集器Dep,用于在这个对象本身发生变化时通知依赖更新this.dep = new Dep();//标记这个对象已经被响应式处理过,防止重复处理object.defineProperty(value, '__ob__', {value: this,enumerable: false,});//如果是数组if (Array.isArray(value)) {//重写数组方法实现响应式this.augmentArray(value);//递归监听数组元素 this.observeArray(value);} else {//如果是普通对象,给对象的所有袁术添加getter.setter this.walk(value);}}//对象属性递归响应式处理walk(obj){Object.keys(obj).forEach(key=>{defineReactive(obj,key,obj[key]);})}//重写数组原型方法 augmentArray(arr){arr.__proto__=arrayMethods}//递归观察数组元素observeAarray(items){items.forEach(item => observe(item));}
}

 

 四. defineReactive 

defineReactive:  Vue想要追踪某个属性的读取和修改,就必须在这个属性的getter中收集依赖,在setter中通知更新,而defineReactive就是专门用来包裹一个对象的属性,让它具备这些能力 

举个例子

const obj={ }

defineReactive(obj,'msg','hello')


conosle.log(obj.msg); //触发getter, 收集依赖

obj.msg='world'; //触发setter,派发更新

//defineReactive.js 
import { Dep } from './dep.js';
import { observe } from './observe.js';
function defineReactive(obj, key, val) {const dep = new Dep();//每个属性都拥有自己的依赖收集器observe(val); //如果属性值还是对象,递归处理object.defineProperty(obj, key, {get() {//如果现在处于依赖收集阶段if (Dep.target) {dep.depend(); //依赖收集}return val;},set(newVal) {if (newVal === val) return;val = newVal;//对新值进行递归响应式处理,如果它是对象或数组,并赋值给childObchildOb = observe(newVal);//通知依赖当前属性的Watcher(计算属性,渲染函数,侦听器等)重新执行 dep.notify();}});
}

五. Dep: 依赖收集与派发更新

Dep是一个依赖收集器,它的主要职责是: 

(1) 存储观察者: Dep实例内部维护了一个观察者(Watcher)对象的数组,在依赖收集阶段,观察者对象会被添加到Dep实例的数字中,而在派发更新阶段,Dep类则会遍历这个数组,通知所有的观察者
(2)依赖收集: Dep类提供了addSub方法,用于在依赖收集阶段添加新的观察者,当数据的getter函数被调用时,Dep会把当前正在评估的观察者添加到自身的观察者列表中
(3)派发更新: Dep类提供了notify方法,用于在数据发生变更时通知所有的观察者,当数据的setter函数被调用时,Dep会遍历自己观察者列表,并调用它们的update方法 
let uid = 0;
class Dep {//每个Deo实例代表一个"响应式属性"的依赖容器//每一个被defineReactive()包括的属性都会对应一个Dep实例//用于存放所有依赖这个属性Watcher(比如组件渲染函数,计算属性,侦听器)//用于给每个Dep实例生成唯一ID(调试用,无功能性作用)constructor() {this.id = uid++;//用于存放所有依赖这个属性的Watcherthis.subs = [];}//把某个Watcher添加到当前Dep的依赖列表中//这个方法一般在Watcher.addDep(dep)中调用addSub(sub) {this.subs.push(sub);}//如果Dep.target不为空(代表当前有一个Watcher正在运行),就调用它哦addDep(this)//换句话说: 这个Dep告诉当前Watcher:"我被你用到了,你得订阅我"//注意: 不是dep.addSub(watcher),而是watcher.addDep(this)depend() {if (Dep.target) {Dep.target.addDep(this);}}//数据变化时触发通知,让所有依赖的Watcher执行更新逻辑(update())notify() {this.subs.forEach(sub => sub.update());}
}
  • subs 数组保存所有依赖(Watcher)
  • 依赖收集通过dep.depend( )和全局Dep.target配合完成; 
  • 数据变化时调用notify( ),触发所有watcher更新 

六.观察者 Watcher

Watcher是一个关键部分,它用于在数据变化时执行更新的操作,其主要作用是在依赖收集阶段将自己添加到每个相关数据的Dependent(Dep)对象中,并在数据变化时接收通知,从而出发回调函数 

主要职责: 
          (1) 依赖收集: Watcher在初始化时会调用自己的get方法去读取数据,这会触发数据的getter函数从而进行依赖收集,在getter函数中,当前Watcher实例会被添加到数据对一个的Deo实例中
          (2)执行更新: 当数据发生变化,Dep实例调用notify方法时, Watcher实例会接收到通知,然后调用自己的update方法以触发回调 
let watcherId = 0;class Watcher {constructor(vm, expOrFn, cb) {//为每个watcher分配唯一id(用于优化)this.id = watcherId++;//当前组件实例 this.vm = vm;this.getter = expOrFn; //表达式函数或渲染函数,比如render或某个计算属性getter this.cb = cb;//数据更新后调用的回调,比如更新DOM this.deps = [];//当前Watcher依赖了哪些Dep this.get(); //初次执行getter,触发依赖收集 }get() {Dep.target = this; //当前正在求值的watcher(静态属性)this.getter.call(this.vm); //执行getter,触发data的getter,从而进行依赖收集 Dep.target = null; //清空target,避免污染其他依赖收集}//每个响应式数据(通过defineReactive实现)在getter被触发时,会把Dep.target添加到自己的subs(订阅者列表)中,//最终完成Dep记录Watcher,也就是Watcher鼎娱乐Dep //添加依赖 addDep(dep) {dep.addSub(this); //将当前watcher添加到Dep的订阅者列表中this.deps.push(dep); //记录依赖了哪个Dep(用于后续取消依赖)}update() {//这里执行视图更新逻辑,调用回调 this.cb();}//当某个响应式数据发生变化,它的dep.notify()方法会被调用//notify() 会遍历所有订阅它的Watcher,执行他们的update()方法}
  • 创建watcher后会立即执行get( )进行依赖收集
  • 依赖数据的getter被调用时,收集watcher; 
  • 数据变化时调用dep.notify( )时,watcher.update()被触发,更新视图

七. 数组响应式的实现(结合Observer和Dep)

  • Observer会为数组替换原型,绑定重写后的变异方法
  • 这些方法执行时调用dep.notify( ),通知依赖更新; 
  • 数组内部的对象元素也会被递归观察,实现深度响应式

1.arraymethods 是vue2中通过劫持数组原型方法来模拟数组响应式

// arraymethods.js //获取原始的Array原型,用于保留原生方法引用
const arrayProto = Array.prototype;//创建一个新对象,继承自原生数组原型
//我们将在这个对象上"重写"某些变更方法 const arrayMethods = Object.create(arrayProto);// 需要被重写的 7 个数组变更方法
const methodsToPatch = ['push',    // 尾部插入'pop',     // 尾部删除'shift',   // 头部删除'unshift', // 头部插入'splice',  // 插入/删除指定位置'sort',    // 排序'reverse'  // 反转
];//遍历每一个方法,进行重写
methodsToPatch.forEach(function (method) {//保留原始方法的引用,稍后调用 const original = arrayProto[method];//在arrayMethods 上定义一个新的同名方法 Object.defineProperty(arrayMethods, method, {value: function mutator(...args) {//执行原始方法,拿到其返回值 const result = original.apply(this, args);//拿到当前数组的Observer实例const ob = this.__ob__;//用于存储新插入的元素(如果有)let inserted;switch (method) {case 'push':case 'unshift':// 新元素全部在参数中inserted = args;break;case 'splice':// splice(start, deleteCount, ...inserted)// 插入的新元素从第三个参数开始inserted = args.slice(2);break;}//对插入的新元素做响应式处理if (inserted) ob.observeArray(inserted);//通知依赖更新,触发视图刷新ob.dep.notify();//返回原始方法的执行结果 return result;},enumerable: false,writable: true,configurable: true});
});

在Observer中使用

if(Array.isArray(value)){

    protoAugment(value,arrayMethods);

    this.observeArray(value)

}

八. 工作流程总结

  1. Vue初始化数据时,调用observe(data); 
  2. 对象每个属性调用defineReactive,加getter/setter;
  3. 组件渲染时,会创建对应的Watcher,并执行渲染函数
  4. 渲染函数访问数据属性,触发getter,Dep.target收集依赖Watcher; 
  5. 数据被修改,setter调用dep.notify( ),触发所有依赖watcher执行update( )
  6. watcher执行视图更新,组件自动刷新

相关文章:

理解 Vue 2 的响应式原理:数据劫持与依赖收集的背后

在Vue2中,响应式系统是一切魔法的源头,无论是模板中的数据绑定,还是computed,watch的精准监听,都离不开Vue背后的响应式机制,本文将从源码角度出发,结合实例,深入剖析vue2是如何通过数据劫持(Object.defineProperty)和依赖收集实现响应式的 一.Vue2响应式系统基本原理 vue2中…...

深入理解 Pinia:Vue 状态管理的革新与实践

深入理解 Pinia&#xff1a;Vue 状态管理的革新与实践 一、引言 在 Vue.js 应用开发中&#xff0c;状态管理是构建复杂应用的关键环节。Pinia 作为新一代 Vue 状态管理库&#xff0c;凭借其简洁的 API 设计、强大的开发体验和良好的性能表现&#xff0c;逐渐成为 Vue 开发者的…...

Dubbo高频面试题

引言 作为分布式服务框架的标杆&#xff0c;Dubbo凭借其高性能RPC通信、灵活的服务治理能力和丰富的容错机制&#xff0c;成为Java技术栈中微服务领域的核心考点。本文系统梳理Dubbo高频面试核心知识点&#xff0c;涵盖容错策略、负载均衡、注册中心原理、服务上下线感知等关键…...

Allegro X PCB设计小诀窍--05.如何在Allegro X中实现隐藏电源飞线效果

背景介绍&#xff1a;在PCB设计过程中&#xff0c;布线初期印制板上的飞线错综复杂&#xff0c;信号线和电源线混合交错&#xff0c;但是实际上对于多层板来说&#xff0c;电源的网络一般是通过电源层铺铜连接的&#xff0c;很少需要走线&#xff0c;这样混乱的情况会严重影响设…...

一篇文章教会你ESP8266串口WIFI无线模块实现物联网无线收发,附STM32代码示例

目录 一、ESP-01S无线模块: &#xff08;1&#xff09;特点&#xff1a; &#xff08;2&#xff09;管脚定义&#xff1a; &#xff08;3&#xff09;启动模式&#xff1a; 二、ESP-01S出厂固件烧录&#xff1a; &#xff08;1&#xff09;引脚接线&#xff1a; &#xff0…...

算法-基础算法

一、枚举算法 也称为穷举算法&#xff0c;指的是按照问题本身的性质&#xff0c;一一列举出该问题所有可能的解&#xff0c;并在逐一列举的过程中&#xff0c;将它们逐一与目标状态进行比较以得出满足问题要求的解。在列举的过程中&#xff0c;既不能遗漏也不能重复 1. 问题 …...

特种设备作业人员-G3锅炉水处理如何备考学习?

备考特种设备作业人员 - G3 锅炉水处理可以从了解考试信息、掌握基础知识、选择学习资料、制定学习计划等多个方面入手&#xff0c;以下是具体的建议&#xff1a; ​ ​1.了解考试信息 *明确考试大纲&#xff1a;详细了解 G3 锅炉水处理考试大纲的要求&#xff0c;明确考试的…...

Reactor模式详解:高并发场景下的事件驱动架构

文章目录 前言一、Reactor模式核心思想二、工作流程详解2.1 服务初始化阶段2.2 主事件循环2.3 子Reactor注册流程2.4 IO事件处理时序2.5 关键设计要点 三、关键实现技术四、实际应用案例总结 前言 在现代高性能服务器开发中&#xff0c;如何高效处理成千上万的并发连接是一个关…...

UniApp 生产批次管理模块技术文档

UniApp 生产批次管理模块技术文档 1. 运行卡入站页面 (RunCardIn) 1.1 页面结构 <template><!-- 页面容器 --><view class"runCardIn" :style"{ paddingTop: padding }"><!-- 页头组件 --><pageHeader :title"$t(MENU:…...

项目日记 -Qt音乐播放器 -设置任务栏图标与托盘图标

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【Qt音乐播放器】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 代码仓库&#xff1a;MusicPlayer v1.0版视频展示&#xff1a;Qt -音乐播放器(仿网易云)V1.0 前言 本文的目标&#xff1a; 一是设置任务栏的图标&#xff0c; 二…...

国产 BIM 软件万翼斗拱的技术突破与现实差距 —— 在创新与迭代中寻找破局之路

万翼斗拱在国产BIM领域迈出重要一步&#xff0c;凭借二三维一体化、参数化建模及AI辅助设计等功能形成差异化竞争力&#xff0c;在住宅设计场景中展现效率优势&#xff0c;但与国际主流软件相比&#xff0c;在功能完整性、性能稳定性和生态成熟度上仍有显著差距&#xff0c;需通…...

记录算法笔记(2025.5.29)最小栈

设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。int get…...

Android SurfaceFlinger核心工作机制

SurfaceFlinger 核心工作机制解析 1. 启动入口与初始化流程 (1) 进程启动入口 二进制文件&#xff1a;/system/bin/surfaceflinger 源码路径&#xff1a;frameworks/native/services/surfaceflinger/main_surfaceflinger.cppint main(int, char**) {// 1. 初始化进程配置sig…...

Golang|etcd服务注册与发现 策略模式

etcd 是一个开源的 分布式键值存储系统&#xff08;Key-Value Store&#xff09;&#xff0c;主要用于配置共享和服务发现。 ETCD是一个键值&#xff08;KV&#xff09;数据库&#xff0c;类似于Redis&#xff0c;支持分布式集群。ETCD也可以看作是一个分布式文件系统&#xff…...

深度解析UniApp盲盒系统开发:从源码架构到多端部署全流程

​一、正版盲盒系统的技术选型与源码设计​ ​跨平台开发框架的核心配置​ ​UniApp多端适配方案​ 环境搭建&#xff1a;全局安装vue/cli与npm install -g dcloudio/uni-cli&#xff0c;通过uni -V验证版本&#xff08;需≥3.0&#xff09;。多端编译命令&#xff1a; # 编译微…...

STM32的OLED显示程序亲测可用:适用于多种场景的稳定显示解决方案

STM32的OLED显示程序亲测可用&#xff1a;适用于多种场景的稳定显示解决方案 【下载地址】STM32的OLED显示程序亲测可用 这是一套专为STM32设计的OLED显示程序&#xff0c;经过实际测试&#xff0c;运行稳定可靠。支持多种OLED屏幕尺寸和类型&#xff0c;提供丰富的显示效果&am…...

【AI News | 20250529】每日AI进展

AI Repos 1、WebAgent 阿里巴巴通义实验室近日发布了WebDancer&#xff0c;一款旨在实现自主信息搜索的原生智能体搜索推理模型。WebDancer采用ReAct框架&#xff0c;通过分阶段训练范式&#xff0c;包括浏览数据构建、轨迹采样、监督微调和强化学习&#xff0c;赋予智能体自主…...

Day12 - 计算机网络 - HTTP

HTTP常用状态码及含义&#xff1f; 301和302区别&#xff1f; 301&#xff1a;永久性移动&#xff0c;请求的资源已被永久移动到新位置。服务器返回此响应时&#xff0c;会返回新的资源地址。302&#xff1a;临时性性移动&#xff0c;服务器从另外的地址响应资源&#xff0c;但…...

Linux驱动学习笔记(十)

热插拔 1.热插拔&#xff1a;就是带电插拔&#xff0c;即允许用户在不关闭系统&#xff0c;不切断电源的情况下拆卸或安装硬盘&#xff0c;板卡等设备。热插拔是内核和用户空间之间&#xff0c;通过调用用户空间程序实现交互来实现的&#xff0c;当内核发生了某种热拔插事件时…...

如何优化Elasticsearch的搜索性能?

优化 Elasticsearch 的搜索性能需要从索引设计、查询优化、硬件配置和集群调优等多方面入手。以下是系统化的优化策略和实操建议: 一、索引设计优化 1. 合理设置分片数 分片大小:单个分片建议 10-50GB(超过50GB会影响查询性能)。分片数量: 总分片数 ≤ 节点数 1000(避免…...

TI dsp FSI (快速串行接口)

简介 快速串行接口&#xff08;FSI - Fast Serial Interface &#xff09;模块是一种串行通信外设&#xff0c;能够在隔离设备之间实现可靠的高速通信。在两个没有共同电源和接地连接的电子电路必须交换信息的情况下&#xff0c;电气隔离设备被使用。 虽然隔离设备促进了信号通…...

责任链模式:构建灵活可扩展的请求处理体系(Java 实现详解)

一、责任链模式核心概念解析 &#xff08;一&#xff09;模式定义与本质 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;其核心思想是将多个处理者对象连成一条链&#xff0c;并沿着这条链传递请求&#xff0c;直到有某…...

nlp中的频率就是权重吗

&#x1f522; 一、“频率”是什么&#xff1f; 在 NLP 中&#xff0c;**词频&#xff08;frequency&#xff09;**通常指的是&#xff1a; 某个单词或 token 在语料库中出现的次数&#xff08;或比例&#xff09; 举例&#xff1a; "The cat sat on the mat. The cat i…...

融智学“新五常”框架:五维方式的重构与协同

融智学“新五常”框架&#xff1a;五维方式的重构与协同 一、理论基底&#xff1a;从传统老五常到当代新五常的范式跃迁 邹晓辉教授提出的新五常&#xff08;生活方式DBA、学习方式DBA、工作方式DBA、旅行方式DBA、娱乐方式DBA&#xff09;&#xff0c;本质是将融智学的核心原…...

wechat-003-学习笔记

1.路由跳转页面&#xff1a;携带的参数会出现在onlaod中的options中。 注意&#xff1a;原生小程序对路由传参的长度也有限制&#xff0c;过长会被截掉。 2.wx.setNavigationBarTitle(Object object) 动态设置当前页面的标题 3.在根目录中的app.json文件中配置 后台播放音乐的能…...

【大模型微调】魔搭社区GPU进行LLaMA-Factory微调大模型自我认知

文章概要&#xff1a; 本文是一篇详细的技术教程&#xff0c;介绍如何使用魔搭社区&#xff08;ModelScope&#xff09;的GPU资源来进行LLaMA-Factory的模型微调。文章分为11个主要步骤&#xff0c;从环境准备到最终的模型测试&#xff0c;系统地介绍了整个微调流程。主要内容包…...

基于MATLAB编程针对NCV检测数据去漂移任务的完整解决方案

以下为针对NCV检测数据去漂移任务的完整解决方案&#xff0c;基于MATLAB编程实现&#xff0c;结构清晰&#xff0c;内容详实&#xff0c;满足技术深度。 NCV信号尾部漂移处理与分析 1. 任务背景与目标 神经传导速度&#xff08;NCV&#xff09;检测信号易受环境干扰与设备漂移…...

【数据结构】哈希表的实现

文章目录 1. 哈希的介绍1.1 直接定址法1.2 哈希冲突1.3 负载因子1.4 哈希函数1.4.1 除法散列法/除留余数法1.4.2 乘法散列法1.4.3 全域散列法 1.5 处理哈希冲突1.5.1 开放地址法1.5.1.1 线性探测1.5.1.2 二次探测1.5.1.3 双重探测1.5.1.4 三种探测方法对比 1.6.3 链地址法 2. 哈…...

永磁同步电机控制算法--基于电磁转矩反馈补偿的新型IP调节器

一、基本原理 先给出IP速度控制器还是PI速度控制器的传递函数&#xff1a; PI调节器 IP调节器 从IP速度控制器还是PI速度控制器的传递函数可以看出&#xff0c;系统的抗负载转矩扰动能力相同,因此虽然采用IP速度控制器改善了转速环的超调问题&#xff0c;但仍然需要通过其他途…...

RabbitMQ 应用 - SpringBoot

以下介绍的是基于 SpringBoot 的 RabbitMQ 开发介绍 Spring Spring AMQP RabbitMQ RabbitMQ tutorial - "Hello World!" | RabbitMQ 工程搭建步骤: 1.引入依赖 2.编写 yml 配置,配置基本信息 3.编写生产者代码 4.编写消费者代码 定义监听类,使用 RabbitListener…...