Vue.js 源码(4)——变化侦测相关的API实现原理

323 阅读2分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

前言

因为 Object 和 Array 的变化侦测有一些缺陷,所以 Vue.js 又提供了 $set$delete 方法。本文,我们将深入学习 $watch$set$delete 的实现原理。

$watch

用法

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

vm.$watch(expOrFn, callback, [options]);
vm.$watch('a.b.c',function(newVal, oldVal){
//...
});

选项:deep

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired

选项:immediate

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调

vm.$watch('a', callback, {
  immediate: true
})
// 立即以 `a` 的当前值触发回调

实现原理

vm.$watch 实际是对 Watcher 的一种封装。

Vue.prototype.$watch = function(expOrFn, cb, options){
    const vm = this;
    options = options || {};
    const watcher = new Watcher(vm, expOrFn, cn, options);
    if (options.immediate){
        cb.call(vm, watcher.value)
    }
    return function unwatchFn() {
        watcher.teardown();
    }
}

expOrFn 我们之前见过,它可以是一个形如 'a.b.c' 的 keyPath,也可以是个函数。执行 $watch 的最后会返回一个 unwatch 函数,内部会调用 watcher.teardown()

这个方法是取消监听,之前没有实现过。实际上取消监听,就是从 dep 中移除该 watcher。那么我们要如何实现呢?

export default class Watcher {
    constructor(vm, expOrFn, cb){
        this.vm = vm;
        this.deps = []; // 新增
        this.depIds = new Set(); // 新增
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.cb = cb;
        this.value = this.get();
    }
    
    addDep(dep) {
        const id = dep.id;
        if (!this.depIds.has(id)){
            this.depIds.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
}

我们在 watcher 中新增 deps 列表,用来记录哪些 dep 收集了该 watcher。我们使用 depIds 来判断如果当前 Watcher 已经订阅了该 Dep,则不会重复订阅。直接 this.ddepIds.add 记录当前 Watcher 已经订阅了 Dep。

Watcher 新增了 addDep 方法后,Dep 中收集依赖的逻辑也需要有所改变:

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

  depend() {
    if (window.target) {
      window.target.addDep(this);
    }
  }
}

Dep 记录了需要通知哪些 Watcher,同时 Watcher 中也记录自己会被哪些 Dep 通知。Watcher 和 Dep,它们是多对多的关系

为什么是多对多的关系?

如果 watcher 中的 expOrFn 参数是一个表达式,那么就只会记录一个 Dep。但如果 expOrFn 是一个函数,函数内部使用了多个数据,那么 Watcher 中就会记录多个 Dep。

this.$watch(function(){
    return this.surname + this.firstName
})

现在,我们可以来实现 teardown

teardown(){
    let i = this.deps.length;
    while(i--){
        this.deps[i].removeSub(this)
    }
}

deep 的实现原理

export default class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    if (options) {
      this.deep = !!options.deep;
    } else {
      this.deep = false;
    }

    this.deps = [];
    this.depIds = new Set();
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }

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

我们需要在 window.target = undefined 之前,使用 traverse 递归 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);
  if ((!isA && isObject(val)) || Object.isFrozen(val)) {
    return;
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.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);
    while (i--) {
      __traverse(val[keys[i]], seen);
    }
  }
}

$set

用法

vm.$set(target, key, value);

之前我们已经学过,只有已经存在的属性的变化会被追踪到,新增的属性无法被追踪到。

vm.$set 就是为了解决这个问题而出现的。它可以将新增的属性也转换成响应式的。

实现原理

Array 的处理

function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
}

Object 的处理

1. key 已经存在
function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // 新增
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
}
2. key 是新增的
function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }

  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 新增
  const ob = target.__ob__;
  if (target.__isVue ||(ob && ob.vmCount)) return val;
  if(!ob) {
    target[key] = val;
    return val;
  }
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}

首先,我们先尝试获取 value 的 __ob__ 属性。

如果不存在,就是普通对象,直接赋值返回即可。如果存在,则表明这是响应式的,需要调用 defineReative 将新增属性转换成 getter/setter 的形式。

最后,向 target 的依赖进行通知。

$delete

用法

vm.$delete(target, key)

实现原理

Array 的处理

export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
}

Object 的处理

1. key 存在
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  // 新增
  const ob = target.__ob__;
  if (target.__isVue ||(ob && ob.vmCount)) return;
  delete target[key];
  ob.dep.notify();
}
2. key 不存在
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  const ob = target.__ob__;
  // 新增
  if (!hasOwn(target.key)) {
    return;
  }
  delete target[key];
  ob.dep.notify();
}
3. 不是响应式对象
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  const ob = target.__ob__;
  if (!hasOwn(target.key)) {
    return;
  }
  delete target[key];
  // 新增
  if (!ob) return;
  ob.dep.notify();
}

总结

$watch/$set/$delete 是如何实现的

$watch 是对 Watcher 调用形式的一种封装。同时,我们还知道了 Watcher 和 Dep 是多对多的关系。

$set$delete 类似,都要对 Array 和 Object 做不同的处理,处理完之后都是通过 value.__ob__.dep.notify() 来完成对依赖的通知。