vue 快速入門 系列 —— 偵測數據的變化 - [vue api 原理]

其餘章節請看:javascript

vue 快速入門 系列html

偵測數據的變化 - [vue api 原理]

前面(偵測數據的變化 - [基本實現])咱們已經介紹了新增屬性沒法被偵測到,以及經過 delete 刪除數據也不會通知外界,所以 vue 提供了 vm.$set() 和 vm.$delete() 來解決這個問題。vue

vm.$watch() 方法賦予咱們監聽實例上數據變化的能力。java

下面依次對這三個方法的使用以及原理進行介紹。react

Tip: 如下代碼出自 vue.esm.js,版本爲 v2.5.20。無關代碼有一些刪減。中文註釋都是筆者添加。express

vm.$set

這是全局 Vue.set 的別名。向響應式對象中添加一個 property,並確保這個新 property 一樣是響應式的,且觸發視圖更新。api

語法:數組

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

參數:ide

  • {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 快速入門 系列

相關文章
相關標籤/搜索