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

news2024/10/10 22:04:21

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.coloradmin.cn/o/1567838.html

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

相关文章

Qt + VS2017 创建一个简单的图片加载应用程序

简介&#xff1a; 本文介绍了如何使用Qt创建一个简单的图片加载应用程序。该应用程序可以打开图片文件并在界面上显示选定的图片&#xff0c;并保存用户上次选择的图片路径。 1. 创建项目&#xff1a; 首先&#xff0c;在VS中创建一个新的Qt Widgets应用程序项目&#xff0c;并…

Vulnhub:WESTWILD: 1.1

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 dirmap enm4ulinux sumbclient get flag1 ssh登录 提权 横向移动 get root 信息收集 arp ┌──(root㉿ru)-[~/kali/vulnhub] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 0…

Springboot Thymeleaf 实现数据添加、修改、查询、删除

1、引言 在Spring Boot中使用Thymeleaf模板引擎实现数据的添加、修改、查询和删除功能&#xff0c;通常步骤如下&#xff1a; 在Controller类中&#xff0c;定义处理HTTP请求的方法。创建Thymeleaf模板来处理表单的显示和数据的绑定。 2、用户数据添加 1、 在Controller类中…

2024年福建三支一扶报名指南—照片<100kb

2024年福建三支一扶报名指南—照片<100kb

目标检测——监控下的汽车

一、重要性及意义 首先&#xff0c;车辆检测技术是保证视频监控系统正常运行的基础。通过监控摄像头实时获取的图像&#xff0c;可以自动检测出图像中的车辆&#xff0c;并进行车辆类型的分类和识别。这对于优化城市交通管理、实现智能交通系统具有重要意义。此外&#xff0c;…

智能小车测速(3.26)

模块介绍&#xff1a; 接线&#xff1a; VCC -- 3.3V 不能接5V&#xff0c;否则遮挡一次会触发3次中断 OUT -- PB14 测速原理&#xff1a; cubeMX设置&#xff1a; PB14设置为gpio中断 打开定时器2&#xff0c;时钟来源设置为内部时钟&#xff0c;设置溢出时间1s&#xff0c…

通义灵码功能上线:编程挑战中Claude3(opus)领先一步

最近好多AI博主都在推荐通义灵码&#xff0c;我就下载下来体验了一下&#xff0c;而且目前通义灵码暂时不收费&#xff0c;而且还有一个推荐奖励的活动&#xff0c;活动内容如下&#xff1a; 邀请好友达到一定人数&#xff0c;有一些奖励。 我目前看到微软的Azure云有个语音转…

LabVIEW电力设备在线监测系统

LabVIEW电力设备在线监测系统 在电力行业中&#xff0c;变电站的稳定运行对于保障电力系统的安全性和可靠性至关重要。开发了一种基于LabVIEW软件开发的变电站电力设备在线监测系统&#xff0c;实时监控变电站内部的电力设备状态&#xff0c;确保电力传输的高效与安全。通过对…

Java—抽象方法与接口

声明&#xff1a;以下内容是根据B站黑马程序员的Java课程&#xff0b;博主自己的理解整理而成&#xff0c;课程很好&#xff0c;适合初学者学习。 关于此类题目&#xff0c;重要的是识别出用什么来实现&#xff0c;到底是接口还是抽象方法&#xff0c;还是共有的属性等等&…

React之Diff 算法

在 React 中&#xff0c;通过 React.createElement 也能生成一个虚拟 DOM 节点&#xff08;ReactElement&#xff09;。在 React15 及以前&#xff0c;采用了递归的方式创建虚拟 DOM&#xff0c;递归过程是不能中断的。如果组件树的层级很深&#xff0c;递归会占用线程很多时间…

达梦配置ODBC连接

达梦配置ODBC连接 基础环境 操作系统&#xff1a;Red Hat Enterprise Linux Server release 7.9 (Maipo) 数据库版本&#xff1a;DM Database Server 64 V8 架构&#xff1a;单实例1 下载ODBC包 下载网址&#xff1a;https://www.unixodbc.org/ unixODBC-2.3.0.tar.gz2 编译并…

树状数组-数据结构

树状数组 t[x] 节点的父节点为 t[x lowbit(x)] 整棵树的深度为 log2n 1 1 . add(x,k) 给指定的节点x加上k — 动态的维护前缀和 需要从x开始&#xff0c;向上找到所有父节点&#xff0c;值都加上k 2. ask(x) 求取节点x之前的前缀和 求取单点之前的前缀和只需要累加即可 …

redis群集有三种模式

目录 redis群集有三种模式 redis群集有三种模式 分别是主从同步/复制、哨兵模式、Cluster ●主从复制&#xff1a;主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份&#xff0c;以及对于读操作的负载均…

LeetCode | 数组 | 二分查找 | 35.搜索插入位置【C++】

题目链接 题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出…

数据结构——图的应用(最小生成树,最短路径,拓扑排序,关键路径)

目录 1.最小生成树 1.概念回顾——生成树 2.最小生成树概念 2.构造最小生成树 1.MST性质 2.Prim算法 3.Kruskal 算法 4.两种算法比较 3.最短路径 1.两点间最短路径 2.某源点到其它各点最短路径 3.单源最短路径——用Dijkstra算法 4.所有顶点间的最短路径…

Echarts 自适应宽高,或指定宽高进行自适应

文章目录 需求分析 需求 有一个按钮实现对Echarts的指定缩放与拉长&#xff0c;形成自适应效果 拉长后效果图 该块元素缩短后效果图 分析 因为我习惯使用 ref 来获取组件的 DOM 元素&#xff0c;然后进行挂载 <div ref"echartsRef" id"myDiv" :sty…

Shell脚本之基本语法

目录 一、变量定义 变量命名规则&#xff1a; 变量的赋值&#xff1a; 只读变量&#xff1a; 删除变量&#xff1a; 二、变量的类型 自定义变量&#xff1a; 环境变量&#xff1a; 位置参数&#xff1a; 预定义变量&#xff1a; 三、键盘输入 四、数值运算 为什么…

余集和拉格朗日定理

L&#xff1a;一个群的例子&#xff08;在下面的文章中进一步详细介绍&#xff09;;R&#xff1a;约瑟夫路易拉格朗日&#xff08;1736-1813&#xff09;&#xff0c; 一、说明 数学家总是痴迷于根据乍一看似乎完全无关的事实/观察来形成概括。为什么&#xff1f;原因很简单&am…

ideaSSM图书借阅管理系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 SSM 图书借阅管理系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码 和数据库&#xff0c;系统主…

JS-11A/11时间继电器 板前接线 JOSEF约瑟

系列型号&#xff1a; JS-11A/11集成电路时间继电器&#xff1b;JS-11A/12集成电路时间继电器&#xff1b; JS-11A/13集成电路时间继电器&#xff1b;JS-11A/136集成电路时间继电器&#xff1b; JS-11A/137集成电路时间继电器&#xff1b;JS-11A/22集成电路时间继电器&#…