关于数据绑定实例方法

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-02-19

vm.$watch

用于观察一个表达式或computed函数在 Vue.js 实例上的变化。

vm.$watch(exprOrFn, callback, [options])

表达式只接受以点分割的路径,例如a.b.c,如果是一个比较复杂的表达式,可以使用函数代替表达式。

vm.$watch 返回一个请取消观察函数,用来停止触发回调。

var unwatch = vm.$watch('a', (newVal, oldVal) => {})
unwatch();

options 选项有两个,deepimmediate

  • deep: 为了发现对象内部的变化,即设置为true为深度监测。
  • immediate: 是否立即以表达式的当前值触发回调。

示例代码:

Vue.prototype.$watch = function (exprOrFn, cb, options) {
  const vm = this;
  options = options || {};
  const watcher = new Watcher(vm, exprOrFn, cb, options);
  if (options.immediate) {
    cb.call(vm, watch.value); // 立即执行必然没有oldVal
  }
  return function unwatchFn() {
    watcher.teardown();
  }
}

Watcher.js

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    // 新增判断代码
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn;
    } else {
      this.getter = parsePath(expr);
    }
    this.cb = cb;
    this.value = this.get();
  }
}

当 exprOrFn 是字符串类型的 keypath 时, Watcher 会读取这个 keypath 所指向的数据并观察这个数据的变化。

而当 exprOrFn 是函数(如计算属性)时,Watcher 会同时观察 exprOrFn 函数中读取的所有 Vue.js 实例上的响应式数据,也就是说,如果函数从 Vue.js 实例上读取了两个数据(触发了两个数据的getter,Watcher实例被两个Dep收集),那么 Watcher 会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。

而 Watcher 则需要记录自己都订阅了谁,也就是 Watcher 实例被收集到了哪些 Dep 里,然后当 Watcher 不想继续订阅这些 Dep 时,循环自己记录的订阅列表来通知它们(Dep)将自己从它们(Dep)的依赖列表中移除掉。

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    
    this.deps = [];
    this.depIds = new Set();

    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn;
    } else {
      this.getter = parsePath(expr);
    }
    this.cb = cb;
    this.value = this.get();
  }

  addDep(dep) { // 具体解析见下文
    const id = dep.id;
    if (!this.deps.has(id)) {
      this.depIds.add(id);
      this.deps.push(dep);
      dep.addSub(this);
    }
  }
}

上一节的实现里面,每当数据发生变化就会读取数据,而读数据就会再次收集依赖,这就会导致 Dep 中的依赖有重复。这样当数据发生变化时,会同时通知多个 Watcher。为了避免这个问题,只有第一次触发 getter 的时候才会触发依赖。

只有当 Watcher 本身被这个 Dep 订阅过,就通过 this.depIds.addthis.deps.push(dep) 来记录这个Dep.id和Dep,避免被统一Dep触发订阅。最后触发 dep.addSub(this) 来让完成 Dep 对 Watcher 的订阅。

Dep 收集依赖的逻辑也需要有所改变:

let uid = 0;

class Dep {
  constructor () {
    this.id = uid++;
    this.subs  = [];
  }

  depend() {
    if (window.target) {
      // 将订阅的逻辑交给Watcher进行判断
      // 保证一个数据的Dep里面的同一个Watch只会被收集一次
      window.target.addDep(this);
    }
  }
}

因此 Dep 会记录数据发生变化时,需要通知哪些 Watcher,而 Watcher 中也同样记录了自己会被哪些 Dep 通知,其实它们是多对多的关系。因为当 Watch 的是一个 computed 时,Watcher 就可能会访问多个数据。因此这个 Watcher 就会收集多个 Dep,同时这些 Dep 中也会收集该 Watch,这样任意一个数据变化,Watcher 都会收到通知。

this.$watch(function() {
  return this.name + this.age;
}, function(newVal, oldVal) {
  console.log(newVal);
  console.log(oldVal);
})

接下来,需要实现 teardown 方法,用来通知 Dep ,让它们把自己(Watcher实例)从依赖列表中移除掉。

class Watcher {
  // ...

  // 从所有依赖项的Dep列表中将自己移除
  teardown() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].removeSub(this);
    }
  }
}
// Dep
class Dep {
  removeSub(sub) {
    const index = this.subs.indexOf(sub);
    if (index > -1) {
      return this.subs.splice(index, 1);
    } 
  }
}

Watcher 想监听某个数据,就会访问某个数据,触发它的getter,将自己收集进去,然后当它发生变化时,就会通知 Watcher。

deep 选项

要想实现 deep 的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑,还要将当前监听的这个值在内的所有子属性都触发一遍依赖收集。

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm;

    if (options) {
      this.deep = !!options.deep;
    } else {
      this.deep = false;
    }
    
    this.deps = [];
    this.depIds = new Set();
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn;
    } else {
      this.getter = parsePath(expr);
    }
    this.cb = cb;
    this.value = this.get();
  }

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

    if (this.deep) {
      // 将当前Watcher收集到子值的依赖列表中
      traverse(value)
    }

    window.target = undefined;
    return value;
  }
}

const seenObjects = new Set();

export function traverse(val) {
  _traverse(val, seenObjects)
  seenObjects.clear();
}

function _traverse(val, seen) {
  let i, keys;
  const isA = Array.isArray(val);

  // 如果不是数组并且不是对象 或者已经被冻结 就直接return
  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
    return;
  }

  if (val.__ob__) {
    const depId = val.__ob__.id;
    if (seen.has(depId)) {
      return;
    }

    seen.add(depId);
  }

  if (isA) {
    i = val.length;
    while (i--) {
      _traverse(val[i], seen);
    }
  } else {
    keys = Object.keys(val)
    i = keys.length;
    while (i--) {
      _traverse(val[keys[i]], seen);
    }
  }
}
// 由于这些子值这也会被遍历追踪 当这里遍历的时候 val[i] 或者 val[keys[i]] 被访问的时候(触发getter,执行 depend)
// 此时 window.target 还没有被置空。
// 因此这些子值就会将当前 Watcher 收集进去 

vm.$set

由于受限于 Object.defineProperty(),只有已存在的属性的变化会被追踪到,新增的属性无法被追踪到。

vm.$set 就是为了解决这个问题而出现的。

Vue.prototype.$set = function (target, key, val) {
  // 如果是数组并且key是一个有效的索引值
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 取大值设置数组length属性
    target.length = Math.max(target.length, key);
    // 使用 splice 进行设置,因为splice会被拦截,就会侦测到target发生了变化触发watcher
    // 也会将新增的val转换成响应式的。
    target.splice(key, 1, val);
    return val;
  }

  // 如果是对象且是“自己”的已经存在的key
  // 是已经存在的属性,即已经是响应式的,直接修改就好
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }

  // 如果是对象,且是新增属性
  const ob = target.__ob__;
  // target 不能是Vue实例或Vue实例的根数据对象(this.$data)
  // 通过 _isVue 判断是不是Vue实例
  // 通过 ob.vmCount 判断是不是根数据对象
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' + 
      'at runtime - declare it upfront in the data option.'
    )
    return val;
  }

  // 如果不是响应式对象,直接设置即可
  if (!ob) {
    target[key] = val;
    return val;
  }

  // 下面就是处理:是在响应式数据上新增了一个属性
  // 直接将新增属性转换为响应式数据
  defineReactive(ob.value, key, val);
  // 通知watcher,target发生了变化,(新增了一个属性)
  ob.dep.notify();
  return val;
}

vm.$delete

vm.$set 一样, 是为了解决当删除一个对象属性时,Vue 无法监听到的问题。

Vue.prototype.$delete = function (target, key) {
  // 如果是数组,直接使用方法进行删除,因为数组的splice方法是被代理过的
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return;
  }

  const ob = target.__ob__;
  // 也不可以在Vue实例和根数据对象上使用
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' + 
      '- just set it of null.'
    )
    return;
  }

  // 如果key不是target自身的属性,则终止程序继续执行
  if (!hasOwn(target, key)) {
    return;
  }

  delete target[key];

  // 如果不是一个响应式数据,则只删除,不通知,也无法进行通知。
  if (!ob) {
    return;
  }

  ob.dep.notify(); // 手动通知
}