Vue 响应式原理

数据劫持

observe

作为入口函数主要作用是判断是否有嵌套的需要监听劫持。

监听的值如果不是对象则直接 return,如果是对象则生成 Observer 实例。

Observer

作用数据的监听对传入对象进行遍。

通过 defineReactive 方法劫持对象中的每一个属性。

defineReactive

对属性进行劫持添加 getter 和 setter.

需要注意的是这里需要在刚进入时调用 observe 方法判断是否需要进一步监听。

还有一点是在 setter 中对属性赋了新的值以后,也需要对新的值调用 observe 方法判断是否需要进一步监听。

img

收集依赖

这里需要用到 Watcher。Watcher 起到的作用其实就是依赖数据并在数据变化时可以执行一个回调进行后续操作。

每个数据都需要维护一个数组,这个数组就是依赖自己的 watcher,所以收集依赖最好的方法就是在defineReactive 的 getter 中实现。为了方便管理,将这个数组抽象为一个 Dep 类,这样它的实例就可以使用一些共用的方法,也就转变为每个数据需要维护自己的 Dep 实例。

派发更新

当对象中的某个数据的 setter 被调用时就说明,数据发生了变化,所以在 setter 中遍历该数据所管理的 Dep 实例,(里面存储的都是 watcher),调用 watcher 中设置好的回调函数,实现响应式更新。

img

需要注意的地方

1.由于是在模板编译函数中的实例化watcher的 ,所以在数据的 getter 中拿不到 Watcher 实例的,这里就在 Watch 的 getter 中将 window.target 设置为当前实例,放在了全局对象上就可以拿到了。

2.添加了实例 watcher 之后 window.target 需要重置,不然会出现一种情况:有一个对象obj: { a: 1, b: 2 }我们先实例化了一个watcher1watcher1依赖obj.a,那么window.target就是watcher1。之后我们访问了obj.b,会发生什么呢?访问obj.b会触发obj.bgettergetter会调用dep.depend(),那么obj.bdep就会收集window.target, 也就是watcher1,这就导致watcher1依赖了obj.b,但事实并非如此。所以在之后可以设置为 null,而在 dep 的添加依赖的方法中设置添加非空的前置条件, 由于js单线程的特性,同一时刻只有一个watcher的代码在执行,因此window.target就是当前正在处于实例化过程中的watcher

在 Vue2.x 的源码中使用的是栈的方式实现父子组件之间切换相应 watcher 的。

3.回顾一下vm.$watch方法,我们可以在定义的回调中访问this,并且该回调可以接收到监听数据的新值和旧值,因此做如下修改

update() {
  const oldValue = this.value
  this.value = parsePath(this.data, this.expression)
  this.cb.call(this.data, this.value, oldValue)
}

总结

  1. 调用observe(obj),将obj设置为响应式对象,observe函数,Observe, defineReactive函数三者互相调用,从而递归地将obj设置为响应式对象
  2. 渲染页面时实例化watcher,这个过程会读取依赖数据的值,从而完成在getter中获取依赖
  3. 依赖变化时触发setter,从而派发更新,执行回调,完成在setter中派发更新

完整代码

// 调用该方法来检测数据
function observe(data) {
  if (typeof data !== "object") return;
  new Observer(data);
}

class Observer {
  constructor(value) {
    this.value = value;
    this.walk();
  }
  walk() {
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key));
  }
}

// 数据拦截
function defineReactive(data, key, value = data[key]) {
  const dep = new Dep();
  observe(value);
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      dep.depend();
      return value;
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return;
      value = newValue;
      observe(newValue);
      dep.notify();
    },
  });
}

// 依赖
class Dep {
  constructor() {
    this.subs = [];
  }

  depend() {
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  notify() {
    const subs = [...this.subs];
    subs.forEach((s) => s.update());
  }

  addSub(sub) {
    this.subs.push(sub);
  }
}

Dep.target = null;

const TargetStack = [];

function pushTarget(_target) {
  TargetStack.push(Dep.target);
  Dep.target = _target;
}

function popTarget() {
  Dep.target = TargetStack.pop();
}

// watcher
class Watcher {
  constructor(data, expression, cb) {
    this.data = data;
    this.expression = expression;
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    pushTarget(this);
    const value = parsePath(this.data, this.expression);
    popTarget();
    return value;
  }

  update() {
    const oldValue = this.value;
    this.value = parsePath(this.data, this.expression);
    this.cb.call(this.data, this.value, oldValue);
  }
}

// 工具函数
function parsePath(obj, expression) {
  const segments = expression.split(".");
  for (let key of segments) {
    if (!obj) return;
    obj = obj[key];
  }
  return obj;
}

// for test
let obj = {
  a: 1,
  b: {
    m: {
      n: 4,
    },
  },
};

observe(obj);

let w1 = new Watcher(obj, "a", (val, oldVal) => {
  console.log(`obj.a 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`);
});
Last Updated:
Contributors: jackysei