vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]

其他章节请看:

vue 快速入门 系列

侦测数据的变化 - [vue api 原理]

前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会通知外界,因此 vue 提供了 vm.$set() 和 vm.$delete() 来解决这个问题。

vm.$watch() 方法赋予我们监听实例上数据变化的能力。

下面依次对这三个方法的使用以及原理进行介绍。

Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。

vm.$set

这是全局 Vue.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。

语法:

  • vm.$set( target, propertyName/index, value )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

以下是相关源码:

Vue.prototype.$set = set;

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
function set (target, key, val) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + 
    ((target))));
  }
  // 如果 target 是数组,并且 key 是一个有效的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 如果传递的索引比数组长度的值大,则将其设置为 length
    target.length = Math.max(target.length, key);
    // 触发拦截器的行为,会自动将新增的 val 转为响应式
    target.splice(key, 1, val);
    return val
  }
  // 如果 key 已经存在,说明这个 key 已经被侦测了,直接修改即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
  // 取得数据的 Observer 实例
  var ob = (target).__ob__;
  // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 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 ' +
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  // 如果数据没有 __ob__,说明不是响应式的,也就不需要做任何特殊处理
  if (!ob) {
    target[key] = val;
    return val
  }
  // 通过 defineReactive$$1() 方法在响应式数据上新增一个属性,该方法会将新增属性
  // 转成 getter/setter
  defineReactive$$1(ob.value, key, val);
  ob.dep.notify();
  return val
}

/**
 * Check if val is a valid array index.
 * 检查 val 是否是一个有效的数组索引
 */
function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

vm.$delete

这是全局 Vue.delete 的别名。删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。你应该很少会使用它。

语法:

  • Vue.delete( target, propertyName/index )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index

实现思路与 vm.$set 类似。请看:

Vue.prototype.$delete = del;
/**
 * Delete a property and trigger change if necessary.
 * 删除属性,并在必要时触发更改。
 */
function del (target, key) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot delete reactive property on undefined, null, or primitive value: " + 
    ((target))));
  }
  // 如果 target 是数组,并且 key 是一个有效的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 触发拦截器的行为
    target.splice(key, 1);
    return
  }
  // 取得数据的 Observer 实例
  var ob = (target).__ob__;
  // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    );
    return
  }
  // key 不是 target 自身属性,直接返回
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key];
  // 不是响应式数据,终止程序
  if (!ob) {
    return
  }
  // 通知依赖
  ob.dep.notify();
}

vm.$watch

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

语法:

  • vm.$watch( expOrFn, callback, [options] )

参数:

  • {string | Function} expOrFn
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate

返回值:

  • {Function} unwatch

例如:

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// 函数
vm.$watch(
  function () {
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)

相关源码请看:

Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    // 通过 Watcher() 来实现 vm.$watch 的基本功能
    var watcher = new Watcher(vm, expOrFn, cb, options);
    // 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + 
        (watcher.expression) + "\""));
      }
    }
    // 返回一个函数,作用是取消观察
    return function unwatchFn () {
      watcher.teardown();
    }
  };

/**
 * Remove self from all dependencies' subscriber list.
 * 取消观察。也就是从所有依赖(Dep)中把自己删除
 */
Watcher.prototype.teardown = function teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this);
    }
    // this.deps 中记录这收集了自己(Wtacher)的依赖
    var i = this.deps.length;
    while (i--) {
      // 依赖中删除自己
      this.deps[i].removeSub(this);
    }
    this.active = false;
  }
};
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    // deep 监听对象内部值的变化
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1; // uid for batching
  this.active = true;
  this.dirty = this.lazy; // for lazy watchers
  // 存储依赖(Dep)。Watcher 可以通过 deps 得知自己被哪些 Dep 收集了。
  // 可用于取消观察
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : '';
  // parse expression for getter
  // expOrFn可以是简单的键路径或函数。本质上都是读取数据的时候收集依赖,
  // 所以函数可以同时监听多个数据的变化
  // 函数: vm.$watch(() => {return this.a + this.b},...)
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  // 键路径: vm.$watch('a.b.c',...)
  } else {
    // 返回一个读取键路径(a.b.c)的函数
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      process.env.NODE_ENV !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get();
};

/**
 * Evaluate the getter, and re-collect dependencies.
 */
Watcher.prototype.get = function get () {
  // 把自己入栈,读数据的时候就可以收集到自己
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    // 收集依赖
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // 对象内部的值发生变化,也需要通知依赖。
    if (this.deep) {
      // 把当前值的子值都触发一遍收集依赖的逻辑即可
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
function traverse (val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse (val, seen) {
  var i, keys;
  var isA = Array.isArray(val);
  // 不是数组和对象、已经被冻结,或者虚拟节点,直接返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    var depId = val.__ob__.dep.id;
    // 拿到 val 的 dep.id,防止重复收集依赖
    if (seen.has(depId)) {
      return
    }
    seen.add(depId);
  }
  // 如果是数组,循环数组,将数组中的每一项递归调用 _traverse
  if (isA) {
    i = val.length;
    while (i--) { _traverse(val[i], seen); }
  } else {
    keys = Object.keys(val);
    i = keys.length;
    // 重点来了:读取数据(val[keys[i]])触发收集依赖的逻辑
    while (i--) { _traverse(val[keys[i]], seen); }
  }
}  

其他章节请看:

vue 快速入门 系列

posted @ 2021-04-02 21:28  彭加李  阅读(286)  评论(0编辑  收藏  举报