Vue 依赖收集

Vue 依赖收集


个人觉得这部分是比较难以理解的,因为当时学的时候也是看了几遍才搞懂这里面的关系。因为我们之前完成了数据劫持和模板编译,而要想实现响应式原理,那么就需要依赖收集,依赖收集分为 基本数据类型的依赖收集和 Object 类型数据(数组和对象)的依赖收集。想要理解依赖收集需要明白:

  • 什么是依赖收集
  • 为什么要依赖收集
  • 怎么依赖收集

本文也将从这三个方面依次讲解,这里会用到之前讲解的数据劫持模板编译的知识,大家不清楚的可以先看看。

1. 什么是依赖收集

接下来的篇幅有点多,一定要认真看,否则会被很多关键词绕晕的😊。

因为根据使用 Vue 的操作来看,我们在改变变量值的时候网页上的内容会跟着改变,想实现这个功能就需要依赖收集了。依赖收集顾名思义,就是收集和**【我】有关系的【依赖】,那么【我】指的是什么呢?在 Vue 中【我】指的就是每个组件,【依赖】指的就是每个组件中在页面中会使用到的数据。一定要注意【页面中】**这个词,因为我们只收集用到的数据,如果这个数据没在页面中用到,那么是不会进行依赖收集的。

上面说的有些抽象,为了更好的理解,我现在举个例子🌰:

  • 按照我们的现有知识(假设你熟悉 Vue 的一点点知识),我们可以知道页面是由一个个**【组件】**组成的,如果只有一个页面,那也是一个根组件,我们一般命名为 App.Vue
  • 接着我们会配合使用 {{ }} 插值语法进行展示我们在 data 中传入的**【变量】**,让这个变量最终以值的形式展现在页面中;
  • 最后,如果用户改变了这个变量的值后(假设这个操作会触发页面的更新),那么在页面上我们会看到最新的值。

这个例子中我们我们可以看到,Vue 让**【组件】【变量】**联系在了一起,这也就是我们经常说的 MVVM 模式。那么现在我们可以抛出两个新的概念:

  • Watcher
  • Dep

不用担心,这两个概念之前一直提到过,只不过换了一个名字罢了,Watcher ?是不是有些耳熟?对就是每个组件里会有的那个 watch 对象,但是这里变成了 Watcher;然后就是 Dep,上过高中的应该学过 dependency 吧?没学过?那depend 总学过吧?翻译过来就是依赖的意思,为啥叫 Dep 呢?写源码的人比较懒吧🤔?

废话不多说,我们把之前提到的所有名词归类可以得到:【我,组件,Watcher】和【依赖,变量,Dep】,现在可以看出来依赖收集是什么了吧?按照我们上面说的就是收集和组件有关的变量,收集和 Watcher 有关的 Dep,所以我们可以得到如下等式:
组件 = W a t c h e r = 我 ; 变量 = D e p = 依赖 组件=Watcher=我;变量=Dep=依赖 组件=Watcher=;变量=Dep=依赖
根据 MVVM 我们又知道组件可以看作为**【视图】,变量可以看作【数据】**,所以综上所述,依赖收集就是每个组件视图中的 Watcher 收集在其中用到的的数据所对应的依赖变量(个人理解)。

花了这么大的篇幅讲解了依赖收集中的两个关键词:Watcher 和 Dep,在接下来你们只要记得 Watcher 对应组件,Dep 对应数据就行了,因为不说清楚,很容易看着看着就晕了。好了,说清楚什么是依赖收集,接下来说说为啥要依赖收集。

2. 为什么要依赖收集

上面说了什么是依赖收集,我们已经知道了那几个关键词的关系了,那我就直接说了。因为我们在开发的过程中一个页面肯定由一个或多个组件组成,那么这个组件中肯定有很多的变量,这些变量中我们可能会用到某些变量,将他们的值展示到页面中,那么我们肯定也想当这些变量发生改变时,页面也跟着一起改变,上面也提到过,为了实现这个需求我们就需要进行依赖收集,也就是收集用到的变量了,因为页面上没用到,你改变它的值肯定是不会去更新视图的,所以也没必要收集它是吧?

同时一个组件中我们肯定会使用很多的变量,这些变量可能会在其他组件中出现,比如用 props 传值等。也就是说组件 1 中可能存在变量 A、B、C,在组件 2 中可能存在变量 C、D、E。那么我们改变变量 C 的值肯定组件 1 和组件 2 肯定要同时更新其视图。因此我们就可以给每个组件在其创建的时候分配一个 Watcher 用它来收集用到的变量 id,当然每一个有用的变量也要收集其所在的组件 id,这样当变量自己发生改变时可以通知自己所在的组件进行视图更新了。可以看如下的图示:

A
Watcher1
B
C
Watcher2
D
E

当 C 发生改变,它要通知它所在的组件 1 和组件 2 进行更新,其他变量只要通知一个组件进行更新即可。

所以为什么要依赖收集呢?为的就是有某个变量进行改变的时候,【我】可以知道是不是要重新渲染自己这个组件。与之前介绍的数据劫持,我们就可以简单的实现响应式了。

3. 怎么依赖收集

记住一句话,这个很重要,记住了你就掌握精髓了:在 get 收集依赖,在 set 渲染视图。

因为之前也提到过,我们只收集页面中用到的变量,那么怎么做到这个呢?我们就可以在 get 中实现了,因为每次获取 data 中的变量,就会触发 get ,那么你可能会有疑问,如果在函数中或者其他地方使用到变量不也会触发吗,那怎么区分这两种场景呢?聪明的尤大大想到了一个好办法,后面会介绍。

接下来我们会介绍依赖收集的具体思路:

  • 在之前我们知道了我们要渲染出真实的 DOM,需要先产生 render 函数,这个 render 就是关键,因为我们在调用 render 函数的时候,会有 _s() 方法来获取 data 上的变量,从而会触发 get 方法,这样我们就可以只获取在页面中用到的变量了;
  • 那么怎么区分 Vue 渲染时的获取操作和用户的获取操作呢?我们可以定义一个全局的唯一变量,你想设成啥都行,但是为了方便我们在 Dep 类下设置了静态属性 Dep.target 来存储当前的 Watcher ,因为我们在每次执行渲染函数,也就是 _update() 函数之前先把 Dep.target 的值设置为当前的 Watcher ,渲染时当出发 get 操作时,因为此时 Dep.target 是有值的,我们就对这个变量进行依赖收集,然后在渲染完后,准确的说是 render 函数执行完就可以了,这样页面使用到的变量我们都会进行依赖收集;
  • 之前也说过 Watcher 要收集 Dep,同时 Dep 也要收集 Watcher ,那么怎么收集呢?这个部分不太好用语言描述,一会可以看具体代码进行解释;
  • 那么你可能会问,我们怎么给每个组件增加一个 Watcher 呢?之前也说过,在每个组件渲染之前增加,也就是在 $mount() 函数里调用 new Watcher 进行组件的实例化,这样每个组件就会有一个 Watcher 实例啦😄。

这样就进行了依赖收集,但是这不是最终的依赖收集,这只是基础版本,因为这个版本会有个问题:

  • 如果一个页面中重复使用了同一个变量多次呢,是要每次都收集一遍吗?答案当然是否定的,那么怎么解决呢,这个可能得再用一个篇幅来介绍;

有了之前的铺垫,你应该对依赖收集的过程有了一个简单的了解,但是你可能还是不太清楚,那么话不多说直接上代码,让你们看懂整个依赖收集的流程。

3.1 组件初始化

之前也说过在渲染前初始化 Watcher ,代码如下:

// 初始化函数
Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = options;
  // 初始化数据,也就是进行数据劫持
  initState(vm)

  // 我们判断用户有没有传入根节点,如果传入那么我们就要进行页面的渲染
  let el = vm.$options.el;
  if (el) {
    // 渲染页面
    vm.$mount(el);
  }
}

// 渲染组件的函数
Vue.prototype.$mount = function (el) {
  // 在这里我们将用户的模板转化为了render函数并挂载到了Vue的options下
  // 渲染组件
  mountComponent(vm);
}

function mountComponent(vm) {
  const updateCompnent = () => vm._update(vm._render());
  const watcher = new Watcher(vm, updateCompnent);
  console.log('-------------\n当前页面的watcher: ', watcher, '\n-------------');
}

这里三个函数不是在一个文件夹里的,写一起只是为了表现在真实场景下的执行顺序,也就是 _init => $mount => mountComponent => new Watcher()

3.2 基础版的依赖收集

// defineReactive.js
function defineReactive(data, key, value) {
  observe(value);
  let dep = new Dep(value); // 通过闭包让每个基本属性都有一个dep实例
  Object.defineProperty(data, key, {
    get() {
      console.log('数据劫持get操作', key, '->', value);
      // 只收集在渲染过程中模板中使用的变量,如果在外获取变量,target的值为null,就不收集
      if (Dep.target) {
        dep.depend(); // (1)
      }
      return value;
    },
    set(newVal) {
      console.log('数据劫持set操作', newVal, '<-', value);
      // 如果没改变值那么直接返回
      if (value === newVal) return;
      // 观察更改的值,让其也变成响应式
      observe(newVal);
      value = newVal;
      dep.notify(); // 如果属性更新了那么就更新视图 (2)
    }
  })
}

// Watcher.js
// 存储组件中的变量
let id = 0;

function Watcher(..., fn, ...) {
  this.id = id++; // 每个watcher的唯一标识符
  this.fn = fn;
  this.deps = []; // 记录watcher中的dep,实现计算属性和清理工作要用到
  ......
  this.mountPage(); // 初次渲染页面 
}

Watcher.prototype.mountPage = function () {
  console.log('渲染页面: run...');
  Dep.target = this;
  this.fn();
  Dep.target = null;
}

Watcher.prototype.update = function () {
  // 不要问为啥,还记得上面说的那个问题吗,这个函数后面会进行重写,目前就是一个简单的更新函数
  this.mountPage();
}

Watcher.prototype.addDep = function (dep) {
  // 让Watcher记住dep
  this.deps.push(dep);
  dep.addWatcher(this);
}
// 收集依赖和通知watcher
let id = 0;
function Dep() {
  this.id = id++; // 每个dep的唯一标识符
  this.subscribe = []; // 存放含有这个变量的watcher
}

Dep.target = null; // 全局唯一变量

Dep.prototype.depend = function () {
  // Dep.target有可能不存在,因为如果为基本类型就直接被return返回了,返回值为undefined
  if (Dep.target) {
    Dep.target.addDep(this); 
  }
}

Dep.prototype.addWatcher = function (watcher) {
  // watcher加了当前dep后,dep把当前的watcher加进去
  this.subscribe.push(watcher);
}

Dep.prototype.notify = function () {
  // 让subscribe里的每个watcher都进行更新
  this.subscribe.forEach(watcher => watcher.update())
}

基础代码就如上面所示,具体的流程就是:

  • 首先在 $mount 中会调用 new Watcher() 那么会直接触发 mountPage 函数,那么会将 Dep.target 赋值为当前的 Watcher

  • 然后在执行 render 函数后会触发 get 函数,也就是运行到上述代码中 (1) 的地方,因为当前是在渲染过程中,所以 Dep.target 是有值的,会触发依赖收集,也就是会运行 depend 函数,然后会让 Watcher 先记住自己,也就是运行 addDep 函数把当前的 dep 添加到 deps 数组里;

  • 然后添加完后,Watcher 会让 dep 记住自己,也就是会调用 addWatcher 函数,让 dep 把自己添加到 subscribe 数组中,这样依赖收集就完成了;

  • 最后只要变量被赋值且不与原来一样就会触发 dep.notify(),也就是上面 (2) 的代码,通知每个 Watcher 进行视图更新,也就是调用 update 函数。

这样就实现了简单的依赖收集和完成了简单的响应式系统,但是这个系统还是有问题的:

  • 如果用户一次性修改了一个或多个变量的值,那么每次赋值都要更新吗?答案肯定是否定的,这样会特别消耗性能,因此之后我们要介绍一部更新策略;
  • 还有我们只能对基础数据进行响应式数据,那么对于数组和对象更新就不会进行页面视图的更新。

4. 写在最后

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 监听Vue数据是指通过Vue提供的监听方法,可以实时监测到数据的变化并做出相应的操作。在Vue中,我们可以使用watch来监听数据的变化。 通过在Vue实例中使用watch属性,我们可以将要监听的数据以字符串形式传递给watch,并在watch中定义一个函数,当数据发生变化时,该函数就会被触发。 例如,假设我们要监听一个叫做"count"的数据,可以在Vue实例中添加如下代码: ``` watch: { count(newValue, oldValue) { console.log('count的值发生变化啦!新的值为:' + newValue + ',旧的值为:' + oldValue); // 可以在这里执行一些自定义的操作 } } ``` 在上述代码中,我们在watch属性中定义了一个count函数,当count数据发生变化时,该函数会被触发。在函数体中,我们可以获取到变化后的新值和变化前的旧值,并可以在这里执行一些自定义的操作。 监听Vue数据可以帮助我们实时获取数据的变化情况,以便及时做出响应。这对于一些需要实时更新数据的场景非常有用,比如当数据发生变化时,我们需要根据新的数据进行计算、展示等操作。 总之,通过监听Vue数据,我们可以在数据变化时得到通知,并进行相应的处理,从而实现数据的实时更新及操作。 ### 回答2: Vue是一种用于构建用户界面的渐进式JavaScript框架。它通过数据驱动和组件化的方式使得开发者可以简单、高效地构建可复用的UI组件。Vue的数据监听机制是其核心特性之一。 Vue通过采用响应式的数据绑定机制来监听数据的变化。当一个数据被绑定到Vue实例中的属性时,Vue会将这个属性转化为getter/setter,并且在需要的时候收集依赖和触发更新。这个机制可以让Vue追踪到每个属性的依赖关系,并在属性发生变化时自动更新有关联的组件。 Vue实现数据监听的方式主要有两种:Object.defineProperty和Proxy。在较低版本的浏览器中,Vue使用Object.defineProperty来实现数据监听。它通过重写对象属性的getter和setter来实现依赖追踪和更新通知。在较新版本的浏览器中,Vue使用Proxy来实现数据监听。Proxy可以劫持整个对象,不需要像Object.defineProperty那样重写getter和setter。 对于Vue来说,数据监听是非常重要的,它可以让开发者编写的代码更具有可维护性和可复用性。当数据发生变化时,Vue会自动检测到这些变化并更新相关的组件,从而实现了数据和视图的双向绑定。这种自动化的数据监听机制可以极大地减少开发者的工作量,提高开发效率。 以一个简单示例来说明数据监听的实现方式。假设有一个Vue实例: ```javascript var vm = new Vue({ data: { message: 'Hello Vue!' } }) ``` 当我们修改`data`中的`message`属性时,例如: ```javascript vm.message = 'Hello World!' ``` Vue会自动检测到`message`属性的变化,并更新对应的组件。在这个示例中,Vue会将`message`属性转化为getter/setter,并在需要的时候触发组件的更新操作,使得界面中显示的内容变为新的值。 综上所述,Vue的数据监听机制是通过响应式的数据绑定方式来实现的,它是Vue框架的核心特性之一,能够实现数据和视图的自动更新,提高开发效率和减少工作量。 ### 回答3: Vue 是一种用于构建用户界面的渐进式JavaScript框架。它包含了一系列的工具和库,使开发人员能够轻松地构建复杂的单页应用程序。Vue 提供了一种数据监听的机制,可以实时地追踪和更新数据的变化。 Vue的数据监听是通过Vue实例中的数据属性来实现的。当数据发生变化时,Vue会自动检测到变化并更新相应的视图。这种监听机制可以确保视图与数据的同步,使开发者能够更加便捷地操作数据和界面。 Vue提供了多种监听数据变化的方式。最常见的方式是使用`v-model`指令将数据绑定到表单元素上,当表单数据发生变化时,Vue会自动更新与之绑定的数据属性。此外,Vue还提供了`watch`属性,可以监听指定数据的变化并执行相应的回调函数。通过`computed`属性,Vue还可以创建计算属性,实时计算数据依赖关系。 除了上述常用方式外,Vue还提供了更高级的数据监听功能。开发者可以使用`$watch`方法手动监听数据的变化。使用该方法,可以监听具体的属性变化,也可以监听整个数据对象的变化。这种方式可以更加灵活地对数据进行监听和操作。 总而言之,Vue提供了灵活而强大的数据监听功能,使开发者能够方便地追踪和处理数据的变化。这为开发人员提供了更好的开发体验和更快速的应用程序开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值