天天看點

Vue Watch 和 Computed 的差別詳解

Vue 提供了Watch 和 Computed兩種元件級别的屬性來供使用者監控資料變化,本文将來分析兩者的使用和差別。

一:文法差別

// Watch的用法by官方文檔
watch: {
  a:function(val,oldval){}
  b:'someMethod'
  c:{
    handler:function(val,oldval){},
    deep:true
  }
  d:{
    handler:function(val,oldval){},
    immediate:true
  }
  e:[
    'handle1',
    function handle2 (val,oldval){},
    {
      handler:function(val,oldval){}
    }
  ],
  'e.f':function(val,oldval){}
}
           
// Computed的用法by官方文檔
computed:{
  aDouble () {
    return this.a * 2
  },
  aPlus: {
    get () {
      return this.a + 1
    },
    set (v) {
      this.a = v-1
    }
  }
}
           

二:使用場景差別

computed 适合 多個資料變化影響一個資料

watch 适合一個資料的變動影響多個資料或者複雜的運算

三:源碼解析

Watch 源碼

注意文中中文注釋

// 初始化資料的時候,會調用一個initWatch方法來初始化使用者定義的Watch
 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);
      }
    }
  }
  // 處理watcher的幾種定義方法,将參數統一
  function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
    // 處理對象的watcher的定義方法
    if (isPlainObject(handler)) { 
      options = handler;
      handler = handler.handler;
    }
    // 處理watch對應的是一個字元串(函數的名稱)從vm中取出function
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    // handle 傳入,變成了watch的callback參數,expOrFn 是監控的變量表達式
    return vm.$watch(expOrFn, handler, options)
  }
  // 實際例子上的$watch方法
 Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
       // 如果cb還是對象,繼續進行調用上面的方法,向下層繼續尋找handle
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      // 建立Watcher,調用下面的watcher構造函數
      var watcher = new Watcher(vm, expOrFn, cb, options);
      // 如果watcher屬性裡定義了immediate,會直接執行watcher的callback也就是handle方法
      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的構造函數

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      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$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      // parsePath 需要處理watcher的表達式,有的時候表達式不一定是vm下的變量名,可能是一個嵌套的變量名,比如a.b.c
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    // 調用get的時候, 會把此watcher加入到響應資料的觀察者清單裡。
    // 這裡如果不清楚,移步到 [響應式原理](https://blog.csdn.net/weixin_41275295/article/details/100626832)
    this.value = this.lazy
      ? undefined
      : this.get(); 
  };
           
Watcher.prototype.get = function get () {
   // 将該watcher加入到全局變量裡,友善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
  };
           

總結下Watch的工作流程

1.初始化元件上配置的watcher屬性

2.對watcher屬性可能的寫法進行規整,得出key和handle

3.通過new Watcher 來建立一個基于key和handle的觀察者

4.Watcher 的key為響應式的vm 上的變量,在watcher.get的時候,watcher訂閱了對應key的變化。完成響應依賴。

5.當key的值發生了變化,觸發watcher的更新方法,并執行回調函數handle

Computed 源碼

// 初始化元件的時候,會調用initComputed方法來初始computed屬性
function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if (!isSSR) {
        // create internal watcher for the computed property.
        // computed 最後本質還是建立一個watcher
        // watcher的expr就是computed key對象的方法
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      } else {
       // 檢測computed裡的key是否在data裡已經有了
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
  }
           
function defineComputed (
    target,
    key,
    userDef
  ) {
    var shouldCache = !isServerRendering();
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key)
        : createGetterInvoker(userDef);
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : createGetterInvoker(userDef.get)
        : noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    // 為computed 的key定義get和set方法
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }
           
// 建立computed 屬性的get方法
function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        // 隻有當資料是舊的時候,才會重新調用watcher的get方法,否則直接傳回value,這裡是有緩存機制的
        // 不會每次都去計算
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          // 将computed的watcher下的所有觀察者dep加入元件watcher的訂閱
          // 這樣可以保證變量更新的同時通知到computed 群組件一起發生變化
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

  function createGetterInvoker(fn) {
    return function computedGetter () {
      return fn.call(this, this)
    }
  }
           

總結computed的流程

1.初始化的時候會擷取computed裡的定義。

2.通過周遊第一步的結果,按照computed新的變量名生成Watcher執行個體。

3.computed的watcher預設是lazy模式的,是以new Watcher 的時候不會調用watcher執行個體的get方法

4.vue 為computed 裡的每個key 代理了一個新的get方法createComputedGetter(),當render頁面的時候,新的get調用computed watcher執行個體的預設get方法。

5.computed執行自定義get方法的時候,會判斷是否依賴又變動,沒有的話,直接取值,否則去執行擷取依賴的變量。

6.擷取依賴變量的時候,将computed的watcher執行個體訂閱到依賴的變量的Dep裡。

7.走完這一步後,再調用計算列的watcher.depend将元件的watcher執行個體也訂閱到計算列依賴的所有變量的dep中。

function createComputedGetter (key) {
    return function computedGetter () {
      debugger
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate(); // 5,6
        }
        if (Dep.target) {
          watcher.depend(); // 7
        }
        return watcher.value // 沒有變化直接取值
      }
    }
  }
           

8.這樣,當變量變化後,會通知computed的watcher将dirty設定為true, 以及元件的watcher更新dom。

四:注意事項

1.watcher 初始化是不執行的,如果想初始化就執行的話可以配置immediate屬性

2.一般情況不要直接修改computed的值,會報錯,一般通過為computed屬性自定義set方法,通過改變依賴變量來改變computed的值

3.computed的屬性如果不加入在dom中渲染是不會被加入到響應系統的。是以如果隻是資料的變動的監控,不映射到dom上,請使用watcher或者其他方法。

4.watcher和computed 屬性定義的函數不能使用箭頭函數,否則内部this會指向元件的父環境,比如window,導緻調用失敗。