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依赖收集是不是有了更深刻的理解呢

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NineData云原生智能数据管理平台新功能发布|2024年3月版

数据库 DevOps - 大功能升级 SQL 开发早期主要提供 SQL 窗口&#xff08;IDE&#xff09;功能&#xff0c;在产品经过将近两年时间的打磨&#xff0c;新增了大量的企业级功能&#xff0c;已经服务了上万开发者&#xff0c;覆盖了数据库设计、开发、测试、变更等生命周期的功能…

python的pip如何升级

升级pip的方法如下&#xff1a; 打开命令行工具。在Windows系统中&#xff0c;可以通过按下WinR键&#xff0c;然后输入"cmd"来打开命令提示符&#xff1b;在Mac或Linux系统中&#xff0c;可以直接打开终端。检查当前pip版本。在终端或命令行中输入以下命令&#…

《C Prime Plus》02

1. UNIX 系统 C语言因UNIX系统而生&#xff0c;也因此而流行&#xff0c;所以我们从UNIX系统开始&#xff08;注意&#xff1a;我们提到的UNIX还包含其他系统&#xff0c;如FreeBSD&#xff0c;它是UNIX的一个分支&#xff0c;但是由于法律原因不使用该名称&#xff09;。 UN…

蓝桥杯备考

目录 P8823 [传智杯 #3 初赛] 期末考试成绩 题目描述 输入格式 输出格式 输入输出样例 说明/提示 代码 P8828 [传智杯 #3 练习赛] 直角三角形 题目描述 输入格式 输出格式 输入输出样例 代码 P8833 [传智杯 #3 决赛] 课程 题目背景 题目描述 输入格式 输出格式…

并发编程之线程池的应用以及一些小细节的详细解析

线程池在实际中的使用 实际开发中&#xff0c;最常用主要还是利用ThreadPoolExecutor自定义线程池&#xff0c;可以给出一些关键的参数来自定义。 在下面的代码中可以看到&#xff0c;该线程池的最大并行线程数是5&#xff0c;线程等候区&#xff08;阻塞队列)是3&#xff0c;即…

鸿蒙OS开发实例:【组件化模式】

组件化一直是移动端比较流行的开发方式&#xff0c;有着编译运行快&#xff0c;业务逻辑分明&#xff0c;任务划分清晰等优点&#xff0c;针对Android端的组件化&#xff1b;与Android端的组件化相比&#xff0c;HarmonyOS的组件化可以说实现起来就颇费一番周折&#xff0c;因为…

还得是抖音,字节推出竖屏视频理解数据集,入选CVPR2024

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 短视频在当下社交媒体逐渐成为主导的视频格式。传统视频处理技术和研究一般都专注于横屏视频…

[HackMyVM]靶场Boxing

难度:Medium kali:192.168.56.104 靶机:192.168.56.143 端口扫描 ┌──(root㉿kali2)-[~/Desktop] └─# nmap 192.168.56.143 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-03 19:25 CST Nmap scan report for staging-env.boxing.hmv (192.168.56.143) Host …

音频转换工具 Bigasoft FLAC Converter for Mac

Bigasoft FLAC Converter for Mac是一款专为Mac用户设计的音频转换工具&#xff0c;它能够将FLAC音频文件高效、高质量地转换为其他常见的音频格式&#xff0c;如MP3、AAC等。这款软件具有直观易用的界面&#xff0c;使用户能够轻松上手&#xff0c;无需复杂的操作步骤即可完成…

Celery的任务流

Celery的任务流 在之前调用任务的时候只是使用delay()和apply_async()方法。但是有时我们并不想简单的执行单个异步任务&#xff0c;比如说需要将某个异步任务的结果作为另一个异步任务的参数或者需要将多个异步任务并行执行&#xff0c;返回一组返回值&#xff0c;为了实现此…

python文件处理:解析docx/word文件文字、图片、复选框

前言 因为一些项目原因&#xff0c;我需要提供解析docx内容功能。本来以为这是一件比较简单的工作&#xff0c;没想到在解析复选框选项上吃了亏&#xff0c;并且较长一段时间内通过各种渠道都没有真正解决这一问题&#xff0c;反而绕了远路。 终于&#xff0c;我在github pytho…

9.图像中值腐蚀膨胀滤波的实现

1 简介 在第七章介绍了基于三种卷积前的图像填充方式&#xff0c;并生成了3X3的图像卷积模板&#xff0c;第八章运用这种卷积模板进行了均值滤波的FPGA实现与MATLAB实现&#xff0c;验证了卷积模板生成的正确性和均值滤波算法的MATLAB算法实现。   由于均值滤波、中值滤波、腐…

v-text 和v-html

接下来&#xff0c;我讲介绍一下v-text和v-html的使用方式以及它们之间的区别。 使用方法 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-widt…

Redis的值有5种数据结构,不同数据结构的使用场景是什么?

文章目录 字符串缓存计数共享Session限速 哈希缓存 列表消息队列文章列表栈队列有限集合 集合标签抽奖社交需求 有序集合排行榜系统 字符串 缓存 &#xff08;1&#xff09;使用原生字符类型缓存 优点&#xff1a;简单直观&#xff0c;每个属性都支持更新操作 缺点&#xff1…

Ansible批量操作(上传文件、删除文件指定文件内容、执行sh文件等)

官方网站 https://www.ansible.com/ 一、Ansible 简介 1、Ansible是新出现的自动化运维工具&#xff0c;完全基于Python开发&#xff0c;集合了众多运维工具&#xff08;puppet、chef、func、fabric&#xff09;的优点&#xff0c;实现了批量系统配置、批量程序部署、批量运行…

4.3学习总结

[HNCTF 2022 WEEK2]Canyource&#xff08;无参数&#xff09; 通过这题又接触了一种无参数RCE的方法&#xff0c;前面学习的getallheaders只有在apache环境下才能使用&#xff0c;具有一定的局限性 这里是利用php函数来构造读取flag的方法 localeconv() – 函数返回一个包含本…

Acrel-1000DP光伏监控系统在尚雷仕(湖北)健康科技有限公司5.98MW分布式光伏10KV并网系统的应用

摘 要&#xff1a;分布式光伏发电特指在用户场地附近建设&#xff0c;运行方式多为自发自用&#xff0c;余电上网&#xff0c;部分项目采用全额上网模式。分布式光伏全额上网的优点是可以充分利用分布式光伏发电系统的发电量&#xff0c;提高分布式光伏发电系统的利用率。发展分…

第19次修改了可删除可持久保存的前端html备忘录:换了一个特别的倒计时时钟

第19次修改了可删除可持久保存的前端html备忘录:换了一个特别的倒计时时钟 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><met…

计算机视觉——图像金字塔理解与代码示例

图像金字塔 有时为了在图像中检测一个物体&#xff08;例如人脸、汽车或其他类似的物体&#xff09;&#xff0c;需要调整图像的大小或对图像进行子采样&#xff0c;并进行进一步的分析。在这种情况下&#xff0c;会保持一组具有不同分辨率的同一图像。称这种集合为图像金字塔…

基于OrangePi Zero2的智能家居项目(开发阶段)

智能家居项目的软件实现 紧接上文 基于OrangePi Zero2的智能家居项目&#xff08;准备阶段&#xff09;-CSDN博客 目录 一、项目整体设计 1.1项目整体设计 1.2具体划分 二、开发工作的前期准备 1、进行分类&#xff0c;并用Makefile文件进行管理 参考&#xff1a;自己创…