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

 

相关推荐

  1. Vue2响应原理

    2024-04-04 12:08:04       22 阅读
  2. vue响应原理依赖追踪

    2024-04-04 12:08:04       47 阅读
  3. vue响应原理

    2024-04-04 12:08:04       36 阅读
  4. Vue 3 响应原理

    2024-04-04 12:08:04       43 阅读
  5. 关于vue3响应依赖注入provide/inject

    2024-04-04 12:08:04       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-04 12:08:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-04 12:08:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-04 12:08:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-04 12:08:04       20 阅读

热门阅读

  1. Qt5.14.2 揭秘Qt日志神器高效诊断程序潜在隐疾

    2024-04-04 12:08:04       14 阅读
  2. C++4.2

    2024-04-04 12:08:04       12 阅读
  3. os模块篇(十二)

    2024-04-04 12:08:04       16 阅读
  4. 5.108 BCC工具之virtiostat.py解读

    2024-04-04 12:08:04       13 阅读
  5. .net 实现的 Webscoket 对象的一些细节和疑问

    2024-04-04 12:08:04       18 阅读
  6. Ideal Holidays

    2024-04-04 12:08:04       15 阅读