3. Vue的watch實現原理

watch的內部原理

Vue在初始化階段會把watch的對象掛載到Vue實例上,並經過initWatch函數初始化vue

initWatch(vm, opts.watch);
複製代碼

經過vm.$options.watch訪問watch的對象內容,具體爲express

function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);
      }
    }
  }
複製代碼

對watch裏檢測的每個屬性建立一個Watcher數組

function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
    // 爲對象時,處理函數是handler,能夠增長可選參數deep,immediate
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }
複製代碼

獲取檢測對象的處理函數handler,並返回一個vm原型中的$watch處理返回值bash

Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      // cb若是是對象的話遞歸建立Watcher
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      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();
      }
    };
複製代碼

上面建立一個Watcher實例,其中expOrFn是函數(在Watcher構造函數中作了判斷處理),當expOrFn是字符串時,Watcher會觀察路徑屬性keypath(如a.b.c)所指向的的數據並觀察這個數據的變化;當數據爲函數時,它會觀察expOrFn所關聯的vue實例上的響應式數據,並隨之變化。而後判斷是否有immediate屬性,有的話就當即觸發回調函數。最後返回一個unwatchFn函數,做用是取消數據觀察函數

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); } var i = this.deps.length; // 從全部依賴項中將本身移除 while (i--) { this.deps[i].removeSub(this); } this.active = false; } }; 複製代碼

實際上在依賴收集的時候,Watcher中會記錄有哪些dep即觀察的屬性,同時Dep類中也會記錄收集了哪些依賴(Watcher),從而能夠像數組刪除元素那樣移除依賴。ui

function remove (arr, item) {
    if (arr.length) {
      var index = arr.indexOf(item);
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
}
Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
};
複製代碼

deep參數的實現

當deep爲true時,會把當前值及其子值都觸發一邊依賴收集,當有任何一個數據變化時,就會通知Watcherthis

Watcher.prototype.get = function get () {
    ...
    // dependencies for deep watching
    if (this.deep) {
        traverse(value);
    }
    popTarget();
    ...
  };
複製代碼

必需要在popTarget()前觸發子值的收集依賴邏輯,才能保證收集的依賴是當前這個Watcherspa

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
    }
    // 前面已講過__ob__表示是響應式數據
    if (val.__ob__) {
      var 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);
      i = keys.length;
      // 遞歸獲取子值,觸發getter,收集依賴,此時Watcher不爲空
      while (i--) { _traverse(val[keys[i]], seen); }
    }
}
複製代碼

至此整個過程結束。prototype

相關文章
相關標籤/搜索