MST

星途 面试题库

面试题:Vue响应式系统中如何追踪依赖

请阐述在Vue的响应式系统里,依赖追踪的基本原理和实现方式,以及Watcher和Dep之间是如何关联并工作的。
21.5万 热度难度
前端开发Vue

知识考点

AI 面试

面试题答案

一键面试

依赖追踪基本原理

  1. 数据劫持:Vue 使用 Object.defineProperty() 方法对数据对象的属性进行劫持,在属性的 gettersetter 中植入依赖收集和触发更新的逻辑。当访问属性时,会触发 getter,从而进行依赖收集;当修改属性时,会触发 setter,进而通知依赖进行更新。
  2. 依赖收集:每个被劫持的属性都对应一个 Dep 实例(依赖管理器),在 getter 中,会将当前正在运行的 Watcher(订阅者)添加到 Dep 中,从而建立起数据与依赖它的 Watcher 之间的关系。

依赖追踪实现方式

  1. 使用 Object.defineProperty():在 Vue 2.x 中,通过遍历对象的属性,使用 Object.defineProperty() 为每个属性定义 gettersetter。例如:
function defineReactive(data, key, value) {
  const dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      value = newValue;
      dep.notify();
    }
  });
}
  1. ES6 Proxy(Vue 3.x):Vue 3.x 采用了 Proxy 来替代 Object.defineProperty()Proxy 可以直接代理整个对象,而不需要对每个属性进行遍历定义,在性能和功能上更强大。例如:
const reactive = (target) => {
  return new Proxy(target, {
    get(target, key) {
      if (Dep.target) {
        const dep = target.__dep__ || (target.__dep__ = new Dep());
        dep.addSub(Dep.target);
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      const dep = target.__dep__;
      if (dep) {
        dep.notify();
      }
      return true;
    }
  });
};

Watcher 和 Dep 之间的关联与工作

  1. 关联
    • Watcher 实例化时,会将自身赋值给 Dep.target,然后访问数据属性,触发属性的 getter,在 getter 中把 Dep.target(即当前 Watcher)添加到 Depsubs 数组(存储所有订阅者)中,这样就建立了 WatcherDep 的关联。
  2. 工作流程
    • 依赖收集阶段:当组件渲染时,会触发数据属性的 getter,此时 Dep.target 是正在渲染组件的 Watcher,属性的 Dep 实例将该 Watcher 添加到自己的订阅者列表 subs 中。
    • 更新阶段:当数据属性发生变化,触发 settersetter 会调用 Dep 实例的 notify 方法,notify 方法遍历 subs 数组,调用每个 Watcherupdate 方法,Watcherupdate 方法会重新计算组件的状态,从而触发组件的重新渲染。例如:
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn);
    this.value = this.get();
  }
  get() {
    Dep.target = this;
    const value = this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
  }
  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}