Vue响应式源码篇(四)几个细节

254 阅读3分钟

当我们改变某个响应式数据触发 setter 时,会执行该数据的 Dep 中的所有 Watcher,也就是会执行 new watcher 时保存的 回调函数如(computed 的 getter)。执行这个回调函数的时候会重新读取依赖的响应式数据,从而触发 getter 进行依赖收集。

data.png

  1. 那么在这个过程中如何保证 Dep 不会重复收集 Watcher 呢?

watcher去重

Vue 采用的方式是给每个 Dep 对象引入 idWatcher 对象中记录所有的 Depid,下次重新收集依赖的时候,如果 Depid 已经存在,就不再收集该 Watcher 了。

let uid = 0;
​
export default class Dep {
    constructor() {
        this.id = uid++;
        this.subs = []; // 保存所有需要执行的函数
    }
    // ......
}
export default class Watcher {
  constructor(Fn) {
    this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
  }
​
  addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  }
}
  1. 如何移除不需要的依赖呢?

依赖清理

Vue 采用的方法是:重新收集依赖时,用一个变量newDeps来记录新一次的依赖,之后再和旧的 Dep 对象列表比对,如果发现多余依赖,就将该依赖的 WatcherDep 中移除。

export default class Dep {
    // ......
    removeSub(sub) {
        remove(this.subs, sub);
    }
    // ......
}
export default class Watcher {
    constructor(Fn) {
        this.getter = Fn;
        this.depIds = new Set(); 
        this.deps = [];
        this.newDeps = []; // 记录新一次的依赖
        this.newDepIds = new Set();
        this.get();
    }
​
    get() {
        Dep.target = this; 
        let value;
        try {
            value = this.getter.call();
        } catch (e) {
            throw e;
        } finally {
            /* 重点关注 */
            this.cleanupDeps();
        }
        return value;
    }
​
    cleanupDeps() {
        let i = this.deps.length;
        while (i--) {
            const dep = this.deps[i];
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this);
            }
        }
        let tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();
        tmp = this.deps;
        this.deps = this.newDeps;
        this.newDeps = tmp;
        this.newDeps.length = 0;
    }
}

在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。

对象添加属性

使用 Object.defineProperty 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的,比如:

var vm = new Vue({
  data:{
    a:1
  }
})
// vm.b 是非响应的
vm.b = 2

Vue 为了解决这个问题,定义了一个全局 API Vue.set 方法。

数组元素添加或删除

对于 vue 无法检测到数组元素的添加或删除的问题,通过将数组的7种原生方法重写为可以截获响应的方法,再将数组的每个成员进行 observe 来解决。

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
​
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
​
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)
​
 /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
    
  /*将数组的原生方法缓存起来,后面要调用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)
​
    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
      
    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})