VUE 状态变化

662 阅读1分钟

image.png

  1. Data通过Observer转换成getter/setter形式追踪数据变化。
  2. 浏览器从Watcher读取数据,Watcher触发getter,getter将Watcher收集到依赖。注意,这里的Watcher是组件,不是一个具体的DOM节点。
  3. 数据发生变化,触发setter,通知Dep中的依赖(Watcher)。
  4. Watcher收到通知,向浏览器发送通知,可能触发视图更新,也可能触发用户的回调。

defineReactive

Object可以通过Object.defineProperty将属性转化成getter/setter 的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter。

需要在getter中收集有哪些依赖使用了数据,当setter被触发时,去通知getter中收集的依赖数据发生了变化。

function defineReactive(data, key, val) {
  let dep = new Dep();

  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend();
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      // 触发依赖
      dep.notify();
    },
  });
}

Watcher

watcher的原理是先把自己设置到全局唯一的指定位置(例如window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的哪个位置读取当前正在读取数据的watcher,并把这个watcher收集到Dep中去。通过这样的方式watcher可以主动去订阅任意一个数据的变化。

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    // 执行this.getter(),就可以读取data.a.b.c的内容
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    window.target = this;
    let value = this.getter.call(this.vm, this.vm);
    window.target = undefined;
    return value;
  }

  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

// Watcher的一个使用方式
vm.$watch("a.b.c", function (newVal, oldVal) {
  // 做点什么
});

// 解析路径
const bailRE = /[^\w.$]/;
export function parsePath(path) {
  if (bailRE.test(path)) {
    return;
  }

  const segments = path.split(".");
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) {
        return;
      }

      obj = obj[segments[i]];
    }
    return obj;
  };
}

Dep

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等

export default class Dep {
  constructor() {
    this.subs = [];
  }

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

  removeSub(sub) {
    remove(this.subs, sub);
  }

  depend() {
    if (window.target) {
      this.addSub(window.target); // watcher
    }
  }

  notify() {
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update();
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}