vue2源码解析——vue中如何进行依赖收集、响应式原理

vue每个组件实例vm都有一个渲染watcher。每个响应式对象的属性key都有一个dep对象。所谓的依赖收集,就是让每个属性记住它依赖的watcher。但是属性可能用在多个模板里,所以,一个属性可能对应多个watcher。因此,在vue2中,属性要通过dep对象管理属性依赖的watcher。在初始化时编译器生成render函数,此时触发属性的依赖收集dep.depend。组件挂载完成后,操作页面,当数据变化后,对应的响应时对象会调用dep.notify方法通知自己对应的watcher更新。在watcher实例中有updateComponent方法,可以进行对应组件的更新。

依赖收集的作用

假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。

let globalObj = {
    text1: 'text1'
};

let o1 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

let o2 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

 这个时候,我们执行了如下操作。

globalObj.text1 = 'hello,text1';

我们应该需要通知 o1 以及 o2 两个vm实例进行视图的更新,「依赖收集」会让 text1 这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。 最终会形成数据与视图的一种对应关系,

dep是跟着key走的还是object走的

在 Vue 中,Dp e对象是跟着对象的属性(key)走的,而不是跟着整个对象走的。每个响应式数据(如 data 中的属性)都会有一个对应的 Dep 实例,Vue 会为对象的每个属性创建一个独立的 Dep 实例来管理依赖关系。当访问对象的某个属性时,Vue 会将该属性对应的 Dep 实例与当前的 Watcher 实例建立关联,从而实现依赖收集和更新机制。

发布-订阅设计模式

在vue2源码设计过程,参考了发布订阅的设计模式。发布订阅和观察者有一个区别,就是发布订阅的发布者和订阅者之间没有直接的依赖关系,通过中间件进行消息传递。观察者模式中,观察对象直接锁定目标,当模板对象发生变化时,直接通知观察者。

发布订阅模式:发布订阅模式中,发布者和订阅者之间的耦合度较低,发布者和订阅者之间通过事件或消息进行通信,彼此不直接依赖。发布订阅模式具有更好的扩展性,可以动态添加新的订阅者或发布者,不影响现有的系统结构。

观察者模式:观察者模式中,目标对象和观察者对象之间的耦合度较高,观察者对象直接订阅目标对象,目标对象需要维护观察者对象的列表。观察者模式在设计时需要明确目标对象和观察者对象之间的关系,扩展性相对较差。

在 Vue 2 中的依赖收集过程中,主要有以下角色:

  1. 发布者(Dep):在 Vue 2 中,Dep(Dependency)充当了发布者的角色。Dep 是一个依赖收集器,用于管理依赖关系。每个响应式数据(如 data 中的属性)都会有一个对应的 Dep 实例,用于存储依赖于该数据的 Watcher 实例。

  2. 订阅者(Watcher):在 Vue 2 中,Watcher 充当了订阅者的角色。Watcher 是一个观察者对象,用于监听数据的变化并执行相应的回调函数。当数据发生变化时,与该数据相关的 Watcher 实例会被通知,从而执行更新操作。

 

 整个过程说人话就是:
初始时模板经过render函数渲染,render过程中,模板new一个watcher实例,并且在Dep这个类中,将该wacher实例赋给Dep.target。

然后渲染过程中对模板中使用到的数据进行响应式定义。就是通过Object.defineProperty那套对对象中的所有属性拦截,重写get和set方法。get和set分别在读取数据和更新数据的时候自动访问到。这个dep对象跟着响应式对象的key属性走的,每个属性key都对应一个dep实例。

在访问数据时触发get方法,将之前存的Dep.target的watcher实例绑定在当前key的dep对象中。在修改数据的时候触发set方法,dep对象更新key所关联的watcher。通过watcher取更新页面。进行组件渲染

伪代码实现

发布者Dep

首先我们来实现一个订阅者 Dep,它的主要作用是用来存放 Watcher 观察者对象。

class Dep {
  constructor() {
    this.subs = []; /* 用来存放Watcher对象的数组 */
    this.target = null; /**用来存放当前watcher对象 */
  }
  addSub(sub) {
    this.subs.push(sub); /* 在subs中添加一个Watcher对象 */
  }
  notify() {
    /* 通知所有Watcher对象更新视图 */
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

为了便于理解我们只实现了添加的部分代码,主要是两件事情:

  1. 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  2. 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

 订阅者Watcher

watcher在实例化后会更新Dep的静态属性target。让Dep.target存储当前渲染的模板watcher

class Watcher {
  constructor() {
    Dep.target =
      this; /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
  }
  update() {
    console.log("更新视图");
  }
}
Dep.target = null;

 Observer和defineReactive

首先在 observer 的过程中会注册 get 方法,该方法用来进行「依赖收集」。在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。

//observer观察对象
function observer(data) {
  if (typeof data != "object" || data == null) {
    return;
  }
  //先不考虑数组,考虑对象的响应式
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key]);
  });
}
//对象的属性key定义响应式
function defineReactive(obj, key, val) {
  observer(val); //递归调用val,防止对象的val也是对象
  const dep = new Dep(); //对每个key生成一个dep实例
  Object.defineProperty(obj, key, {
    //使用defineProperty重写get和set方法
    get: function reactiveGetter() {
      dep.addSub(Dep.target); //将Dep.target当前模板wacher实例
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      observer(newVal);
      dep.notify();
    },
  });
}

 那么「依赖收集」的前提条件还有两个:

  1. 触发 get 方法;
  2. 新建一个 Watcher 对象。

 模拟new Vue()

在template生成-》进行初始化操作=》定义数据响应式=》模板渲染=》挂载前定义好更新方法并为模板创建一个watcher实例。在访问响应式属性的时候,defineReactive里的get方法会将当前wacher加入到dep中。在修改属性的时候,deReactive里的set方法会将利用dep通知wacher,而watcher内部有通知组件更新的方法。

这样就实现了数据发生变化,所有依赖的模板都会更新。

function Vue(options) {
  this._data = options.data;
  observer(this._data);//定义响应式
}
Vue.prototype.$mount = function (el) {
  const options = this.$options;
  const { render } = compileToFunctions(options.template);
  options.render = render;
  return mountComponent(this, el);
};
Vue.prototype._render = function () {
  let vNode = this.$options.render.call(this);
  return vNode;
};
function mountComponent(vm, el) {
  let updateComponent = () => {
    vm._update(vm._render());
  };
  new Watcher(vm, updateComponent);
}
const vm = new Vue();
vm.$mount(el);

源码解析

源码就去看下面两个,一个是vue执行过程,定义响应式、模板渲染、更新的逻辑;在instance文件夹下。一个是响应式、依赖收集的属性和方法,在observer文件下。

如果你想看render函数过程,要看编译时被重写的$mount方法,那里是render函数生成的核心。 vue-main\src\platforms\web\runtime-with-compiler.ts

响应式入口

init.ts文件在new Vue,并且执行._init方法时完成vue一些初始化和生命周期钩子函数。这里就调用了initState方法。这个方法处理vue实例的相关数据,通过调用oberver完成数据的观测,从而进行响应式依赖收集,这是数据响应式的关键入口。

 

 依赖收集入口

_init方法中,最后是不是执行了$mount方法。这个$mount方法中调用了mountComponent。在mountComponent方法里通过new Watcher创建一个watcher实例。这里是依赖收集的入口。

 

Observer类

Observer类通过构造方法,实现了对响应式对象、数组添加响应式的功能。

对于数组重写数组原型的push\pop\splice\unshift\shiift\reverse\sort方法。对于对象通过defineReactive定义对象key的响应式。

defineReactive函数

定义响应式的核心方法,在这个方法中,首先定义一个dep对象,dep是一个闭包,在defineReactive之后后仍然能被get和set方法访问到。

递归处理val,进行响应式观察observe(val)

使用Object.defineProperty定义get方法。在使用属性的时候,通过dep.depend将当前模板渲染watcher加入到key的依赖中。

递归处理子对象。

 

Dep类

dep类是依赖收集的核心。定义了一个target静态变量,全局使用。定一个subs数组,用于存储wathcer实例。并提供了四种方法:addSub\removeSub\depend\notify。

 

dep.depend做了什么

depend是dep对象的方法,先去找了全局变量Dep.target。然后调用Dep.target的addDep方法。这个Dep.target其实是一个watcher对象。在addDep方法里,让dep对象调用了自身的addSub方法将这个Dep.target也就是这个watcher实例加入到subs中。

这块写的好绕对吧,源码实现里,dep对象没有自己去调addSub方法,而是让wacher实例转了个手,wacher实例调自己的addDep,然后这个addDep去调的addSub方法。 为什么呢?因为watcher也想记住它对应哪些dep对象。watcher里维护一个newDeps数组,里面存放了相关的dep对象。所以watcher和dep对象是多对多的关系!

 

Dep.target是什么

前面我们默认这个Dep.target就是当前模板渲染wacher,为什么呢,什么时候放到Dep.target里的了

在dep.ts这个文件里对外抛出了pushTarget方法这里可以修改Dep.target值

 

在源码里查找,发现在watcher的get方法里调用了pushTarget方法 。而get在watcher的构造函数里使用。说明在生成wacher实例的时候,如果不是lazy,watcher执行会自动调pushTarget方法,将Dep.target更新为当前的wacher实例。

 当watcher重新执行或计算的时候会再次调get方法,更新Dep.target

 因此,结论就是,Dep.target就是当前watcher实例对象  

在去看dep.depend方法,除了前面说的addDep绕了一圈将这个Dep.target放到了dep的subs数组里。还调用了Dep.target的,也就是watcher实例的onTrack方法,这个方法用于在追踪依赖时执行额外的调试操作。

dep.notify方法

notify 方法的主要作用是通知所有订阅者进行更新操作,确保它们按照正确的顺序执行更新,并在开发环境下提供调试信息。这样可以保证在数据变化时,所有订阅者都能及时更新自身状态。

  1. 首先方法会对订阅者列表 this.subs 进行稳定化处理,过滤掉可能为 null 的订阅者,并将剩下的订阅者转换为 DepTarget 类型的数组 subs

  2. 如果在开发环境下(__DEV__)且不是异步模式(config.async 为假),则需要对订阅者列表进行排序,以确保它们按正确的顺序触发更新。通过对订阅者数组 subs 按照 id 属性排序,保证它们按照正确的顺序执行。

  3. 遍历订阅者数组 subs,对每个订阅者执行以下操作:

    • 如果在开发环境下且传入了 info 参数,则调用订阅者的 onTrigger 方法(如果存在),并传入包含额外信息的对象 { effect: subs[i], ...info }
    • 调用订阅者的 update 方法,用于执行订阅者的更新操作。

watcher

前面说dep收集的subs订阅者,是不是watcher啊,那watcher是干啥的呢

首先,看下,watcher是在组件挂载前生成的 ,

 

 在响应式数据收集依赖里,我们关注的watcher上的以下几个属性:deps、newDeps数组;depIds、newDepIds的set对象;记录watcher模板关联的dep对象。id集合的作用是保证deps、newDeps的唯一性,防止dep被重复添加。

 

 核心方法——get方法,主要在new Watcher创建实例的时候调用,创建Dep.target=当前wacher实例

 核心方法——addDep,在响应式数据访问的时候,响应式属性通过Object.defineProperty重写的get方法中的dep.depend方法,通过Dep.target拿到当前wacher实例的访问。Dep.target通过调用addDep方法,完成了wacher和dep对象的双向记录。wacher将dep对象记录在当前的newDeps数组中;而dep对象通过调用addSub方法,将wacher实例记录在自己的subs数组中。

 

 核心方法——cleanupDeps,完成wacher和dep对象的相互清除操作

 OK,到这里,你对vue依赖收集是不是有了更深刻的理解呢

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于单片机的超声波测距仪设计_kaic

摘 要 如今社会持续深化转型&#xff0c;在人工智能领域&#xff0c;传感器采集外部数据&#xff0c;经过处理器对数 据运算和处理&#xff0c;从而实现相应的功能。比如自动驾驶技术中&#xff0c;超声波传感器应用广泛&#xff0c; 超声波是一种频率在 20khz 以上的声波&…

如何保护IP地址?安全匿名上网的方法

当互联网成为每个家庭的重要组成部分后&#xff0c;IP地址就成了你的虚拟地址。您的请求从该地址开始&#xff0c;然后 Internet 将消息发送回该地址。那么&#xff0c;您担心您的地址被泄露吗&#xff1f; 对于安全意识高或者某些业务需求的用户&#xff0c;如果您正在寻找保护…

Zabbix6 - Web管理网络拓扑/端口流量监控配置手册

Zabbix6 - Web管理网络拓扑/端口流量监控配置手册 概述: 1)Zabbix能监视各种网络参数,保证服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 Zabbix由两部分构成,Zabbix Server与可选组件Zabbix Agent。通过C/S模式采集数据,通过B…

WEB 工程路径

WEB 工程路径 相对路径 使用相对路径来解决&#xff0c; 一个非常重要的规则&#xff1a;页面所有的相对路径&#xff0c;在默认情况下&#xff0c;都会参考当前浏览器地址栏的路径 http://ip:port/工程名/ 资源来进行跳转。 相对路径带来的问题 如上图&#xff0c;若在a.h…

MySQL进阶-----前缀索引、单例与联合索引

目录 前言 一、前缀索引 1. 语法 2. 如何选择前缀长度 3. 前缀索引的查询流程 二、单列索引与联合索引 三、索引设计原则 前言 本期是MySQL进阶篇当中索引的最后一期内容&#xff0c;这里我们主要接着上一期继续讲解前缀索引、单例与联合索引。&#xff08;上一期链接&…

02 Python进阶:CGI编程

什么是CGI CGI是通用网关接口&#xff08;Common Gateway Interface&#xff09;的缩写&#xff0c;它是一种标准协议&#xff0c;用于Web服务器执行外部程序或脚本与Web浏览器进行交互。通过CGI&#xff0c;Web服务器能够动态生成网页内容&#xff0c;处理用户提交的表单数据…

从零开始 使用OMNET++结合VEINS,INET和SUMO的联合仿真

背景知识 当我们探索未来的交通系统和智能交通解决方案时&#xff0c;车辆到一切&#xff08;Vehicle-to-Everything, V2X&#xff09;通信技术显得尤为重要。V2X是指在车辆与车辆&#xff08;V2V&#xff09;、车辆与基础设施&#xff08;V2I&#xff09;、车辆与行人&#x…

年薪50w的网络安全工程师是如何炼成的?

前言 一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防…

“三电”打不过极氪007、比亚迪海豹?我还是选小米SU7 Pro

文 | AUTO芯球 作者 | 雷歌 我真是越来越烦小米汽车的这帮脑残粉了。 小米SU7热闹的发布会过后&#xff0c; 友商们开始在价格上狙击小米SU7。 这两天&#xff0c; 问界M7降价2万&#xff0c;22万多就能提&#xff0c; 极氪001现在送1.6万的选装配置&#xff0c; 银河E…

ChatGPT 上线新功能:DALL·E 可以编辑图片了

ChatGPT 上线新功能&#xff1a;DALLE 可以编辑图片了。可以对生成的图片内容进行修改、添加和删除。 前几天看到消息说还在内测中&#xff0c;今天就体验上了。 这是官方文档&#xff1a;https://help.openai.com/en/articles/9055440-editing-your-images-with-dall-e 界面…

【fastadmin】脚本模式下,日志钩子函数执行出现死循环,导致内存溢出奔溃

问题出现原因是想对项目中error级别的日志&#xff0c;接入钉钉告警&#xff0c;方便查看 于是使用钩子方法&#xff0c;日志写入完成后&#xff0c;自动调用自定义的告警方法中 1、在application/tags.php 中添加log_write_done > [app\\common\\behavior\\Common, ],2、在…

GPTs构建广告文案Agent(只需要一个网址链接即可生成文案及配图)

在大家已经有账号的前提下&#xff0c;我们来看看怎么做&#xff01;&#xff01;&#xff01; 进入GPTs的编辑界面 如下图&#xff1a; 如何配置呢&#xff1f; Name&#xff1a;给我们的GPTs起个名字。Description&#xff1a;简单介绍一下&#xff0c;我们创建的GPTs是…

家用洗地机选购指南,哪款洗地机性价比高且用户评价好?

在当今快节奏的生活中&#xff0c;无论是商业场所还是家庭&#xff0c;清洁环境都是我们追求的目标之一。而一台高性价比的洗地机在频繁清洁地面时尤为重要。市面上的洗地机种类繁多&#xff0c;如何选择适合自己的洗地机成为了一个挑战。那么&#xff0c;到底哪款牌子的洗地机…

隐语SecretFlow实训营-第9讲:隐语多方安全计算在安全核对的行业实践

业务背景&#xff1a;安全核对产生的土壤 行业背景&#xff1a; 隐私计算技术&#xff0c;实现数据可用不可见、可用不可得。 产品方案&#xff1a;从试点到规模化的路 基于隐语SCQL的数据比对应用&#xff1a;风洞隐私安全核对。 支持 1 to N的规模化核对。 特色功能&…

经典卡尔曼滤波完整公式推导

文章目录 1. 例子1.1 kalman跟踪车道线1.2 鼠标跟踪-匀速运动 2. 卡尔曼滤波3. 卡尔曼公式推导3.1 数学基础1(递归)3.2 数学基础2(数据融合、协方差、状态空间方程、观测器问题)3.3 卡尔曼增益公式推导3.4 误差协方差矩阵3.5 误差来源3.6 滤波调参 运动模型参考资料 1. 例子 1…

【重学C语言】三、C语言最简单的程序

【重学C语言】三、C语言最简单的程序 最简单的程序头文件使用尖括号 < >使用双引号 ""区别与注意事项示例 主函数认识三个错误 常量和变量常量ASCII 码表转义字符 关键字数据类型关键字存储类关键字修饰符关键字控制流程关键字函数相关关键字其他关键字 变量变…

SQL语句生成器,支持MSSQL/MYSQL/SQLITE/ACCESS/EXCEL

经过7个月的艰苦开发&#xff0c;SQL语句生成器终于和各位见面了&#xff0c;因为工程量浩大&#xff0c;一度做到崩溃&#xff0c;差点烂尾&#xff0c;好在经过N次激烈思想斗争后还是坚持了下来累累累累累累累 本软件能够自动生成SQL语句及对应的易语言代码&#xff0c;还有相…

HCIA笔记

console 登录设备的特点&#xff1a; 1、带外&#xff0c;不依赖网络本身的连通性。 2、独占&#xff0c;console口不能被多人同时使用&#xff0c;具备唯一性。 3、本地&#xff0c;console口长度有限&#xff0c;一般只能在机房或者设备现场来使用。 4、只能实现命令行的管理…

安泰电子 :电压放大器的技术指标有哪些

电压放大器是电子设备中常见的一种电路&#xff0c;它的作用是将输入信号的电压放大到所需的输出电压。电压放大器的技术指标描述了其性能和特征&#xff0c;涵盖了许多方面。下面西安安泰将详细介绍一些电压放大器的技术指标。 增益&#xff1a;增益是电压放大器最基本的指标之…

央视曝光!耗资超800万元建高标准农田,两年后改建光伏电站

近期&#xff0c;央视报道揭露湖北部分乡村存在基本农田被光伏项目侵占的情况。报道称湖北孝感市安陆市木梓乡发现光伏项目占用基本农田&#xff0c;却只得每亩500元租金且光伏项目未配套农业种植&#xff0c;导致村民收入减少和噪音扰民的情况。 据央视走访调查该地发现存在工…