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

news2025/6/1 9:43:40

在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;
            //对新值进行递归响应式处理,如果它是对象或数组,并赋值给childOb
            childOb = 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++;
        //用于存放所有依赖这个属性的Watcher
        this.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执行视图更新,组件自动刷新

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2392492.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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. 问题 …

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

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

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

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

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

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

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

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

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;当内核发生了某种热拔插事件时…

TI dsp FSI (快速串行接口)

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

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

一、责任链模式核心概念解析 &#xff08;一&#xff09;模式定义与本质 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;其核心思想是将多个处理者对象连成一条链&#xff0c;并沿着这条链传递请求&#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;系统地介绍了整个微调流程。主要内容包…

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

文章目录 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…

基于递归思想的系统架构图自动化生成实践

文章目录 一、核心思想解析二、关键技术实现1. 动态布局算法2. 样式规范集成3. MCP服务封装三、典型应用场景四、最佳实践建议五、扩展方向一、核心思想解析 本系统通过递归算法实现了Markdown层级结构到PPTX架构图的自动转换,其核心设计思想包含两个维度: 数据结构递归:将…

OpenGL Chan视频学习-9 Index Buffers inOpenGL

bilibili视频链接&#xff1a; 【最好的OpenGL教程之一】https://www.bilibili.com/video/BV1MJ411u7Bc?p5&vd_source44b77bde056381262ee55e448b9b1973 函数网站&#xff1a; docs.gl 说明&#xff1a; 1.之后就不再单独整理网站具体函数了&#xff0c;网站直接翻译会…