Skip to content

依赖收集

依赖收集

依赖收集是Vue数据响应式的核心, 初始化时候会进行依赖收集, 等到数据变更的时候再进行派发更新, 数据响应式就闭环了, 但是需要注意, 再vue2中, 对象和数组的依赖收集方式是不一样的

对象的依赖收集

1. 大体逻辑

首先在首次渲之前会执行_render函数, 执行render函数的时候, render函数内部的变量需要从vm上取值,由于在前面,已经通过observedata内的值进行观察, 取值的时候会执行对应属性的get函数, 改变值的时候会触发对应属性的set函数, 因此在改变值的时候需要知道那部分是依赖这个属性的, 就需要在取值的时候对依赖进行收集, 才能在值改变的时候触发这部分依赖的watcher内的exprFn函数重新执行

vue中, 需要有一个Dep来存放watcher, Dep的角色是一个观察者, 在依赖变更的时候通知对应的watcher更新依赖

那么问题是如何知道当前哪个属性正在取值呢? 由于js是执行时单线程的,同一时间只会有一个属性在取值, 因此在执行render函数的这段时间内, 就知道时哪些在取值

2. pushTarget与popTarget

observe/wathcer.js

js
import { popTarget, pushTarget } from "./dep";
class Watcher{
 // ....
 get() {
   pushTarget(this);
   this.exprFn();
   popTarget();
 }
}
export defaultWathcer

3. Dep

Dep.js

js
let uid = 0;

class Dep {
  static target = null;
  constructor() {
    this.uid = uid++;
 this.subs = [] // 用于存储wathcher
  }
  
  // 依赖收集
  depend() {}
}

export default Dep;

let stack = []; // 用一个栈维护当前正在取值的对象对应的watcher
export function pushTarget(target) {
  Dep.target = target; // target为Dep的静态属性, 同一时间只会有一个
  stack.push(target); // 取值的的时候将当前的watcher放入栈中
}

// 执行完后会调用popTarget方法
export function popTarget() {
  stack.pop(); // 将执执行完成的watcher从栈中移除
  Dep.target = stack[stack.length - 1];
}

4. 依赖收集与派发跟新流程

在observe内部对对象进行依赖收集与派发跟新操作

js
// 定义响应式数据
function defineReavtive(obj, key, val) {
  // 递归观测每一层
  observer(val);

  const dep = new Dep(); // 为每一个val创建一个dep实例

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) { // 在wathcer中调用了get方法后, Dep.target肯定是存在的
  // 依赖收集
        dep.depend();
      }
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      //   有可能设置的新值也是一个对象, 需要对设置的新值进行观测
      observer(newVal);
      val = newVal;
   
   // 派发更新
      dep.notify();
    },
  });
}

get中调用dep.depend()时候, 就是对val进行依赖收集

js
// Dep内部-*/`14
  depend() {
   
    this.subs.push(Dep.target);
  }

set中调用dep.notify()时候, 就是派发跟新, 需要想重新执行watcher中的exprFn函数, 由于这里为渲染Wathcer,因此执行的函数为updateCompunent

js
// Dep内部
// 派发更新
notify() {
 this.subs.forEach((wathcer) => wathcer.update());
}

那么在watcher内部应该就有一个update方法

js
// Watcher内部
  // 更新页面
  update() {
    this.get()
  }

对象的依赖收集流程大致完成, 当然还存在一些问题, 后面再说, 先来看数组的依赖收集

数组的依赖收集

observe中, 区分了对象与数组, 若是数组会递归observe每一项数组元素, 递归过程中就做了依赖收集, 但数组还有一个操作就是数组特有的7个方法, 这7个方法同样也需要进行依赖收集

举个例子: 一个数组,用了push方法后, 新增了一项元素, 这个数组变化了,所以在push方法内可以进行派发更新, 但是这里面没有dep, 要调用dep.notify, 可以将dep添加到当前数组的__ob__身上

js
// 对数组重写方法
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    // 先调用原始的方法
    const res = arrayProto[method].apply(this, args);

    // this表示当前数组 value.push(), 这里的value会走到这里
    const ob = this.__ob__; // this为数组本身

    let inseted;
    switch (method) {
      case "push":
      case "unshift":
        inseted = args;
        break;
      case "splice":
        inseted = args.slice(2);
        break;
      default:
    }

    // 对新增属性进行观测
    if (inseted) ob.observerArray(inseted);
    ob.dep.notify(); // 数组方法派发更新

    return res;
  };
});

observer中, 在对一个值为数组的key取值时候,进到defineReavtive(obbj,key, val)中后, val就是一个数组, 这个数组身上有了__ob__属性, 因此可以针对val.__ob__.dep进行依赖收集与派发跟新了

js

// 定义响应式数据
function defineReavtive(obj, key, val) {
  // 递归观测每一层
  observer(val);

  const dep = new Dep();
  const childOb = val.__ob__; // val为数组本身

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend(); // 数组本身也会被取值,也需要进行依赖收集
        }
      }
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      //   有可能设置的新值也是一个对象, 需要对设置的新值进行观测
      observer(newVal);
      val = newVal;
      dep.notify();
    },
  });
}

到这里, 数组的依赖收集也就完成了

解决依赖重复的问题

问题描述

上述对象和数组的依赖收集存在一个问题, 若一个值在模板中多次使用, 就会被多次收集依赖, 但对于同一个值来说, 依赖都一样,多次收集依赖会造成性能的浪费, 解决方案如下:

  1. 每一个依赖都会有一个Dep实例, 因此给每一个dep添加一个ID,用于去重
  2. 每一个dep中的存储是watcher也应该是唯一的, 因此每一个wathcer也应该有一个唯一的ID
  3. 一个dep身上会有多个watcher, 同样, 一个watcher也属于多个dep, 因此depwatcher的关系为多对多

依赖收集时候的去重策略

js
// Dep内部

// 依赖收集方法进行改造
depend() {
 // this.subs.push(Dep.target); // 这样同一个dep中可能会存再多个一样的watcher, 浪费性能
 // Dep.target 就是当前的watcher
 Dep.target.addDep(this);
}
js
// Wathcher内部

class Watcher {
 constructor() {
  // ....
  this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
  this.depsIds = new Set(); // 一个watcher上面有多个dep, 用于去重
  this.deps = []; // 当前watcher属于哪些dep
 }
 
 // 添加dep
 addDep(dep) {
  const depId = dep.uid;
  if (!this.depsIds.has(depId)) { // 去重操作
   this.depsIds.add(depId);
   this.deps.push(dep); // 将当前watcher与dep建立1对多的关系
   dep.subs.push(this); // dep与watcher也建立1对多的关系
  }
 }
}

到这里, 依赖收集和派发跟新的核心逻辑就写完了, 但依然还存在一些效率问题, 后面再来解决