深入理解vue的watch

深入理解vue的watch

vue中的wactch可以監聽到data的變化,執行定義的回調,在某些場景是很有用的,本文將深入源碼揭開watch額面紗

前言

  • version: v2.5.17-beta.0
  • 閱讀本文需讀懂vue數據驅動部分

watch的使用

當改變data值,同時會引發副作用時,可以用watch。比如:有一個頁面有三個用戶行爲會觸發this.count的改變,當this.count改變,需要重新獲取list值,這時候就可以用watch輕鬆完成

new Vue({
  el: '#app',
  data: {
    count: 1,
    list: []
  },
  watch: {
    // 不管多少地方改變count,都會執行到這裏去改變list的值
    count(val) {
      ajax(val).then(list => {
        this.list = list;
      })
    }
  },
  methods: {
    // 點擊+1,count + 1,刷新列表
    handleClick() {
      this.count += 1;
    },
    // 點擊重置,count = 1,刷新列表
    handleReset() {
      this.count = 1;
    },
    // 點擊隨機, count隨機數,刷新列表
    handleRamdon() {
      this.count = Math.ceil(Math.random() * 10);
    }
  }
})

這樣的好處就是把所有源頭聚集到了watch中,不需要在多個count改變的地方手動去調用方法,減少代碼冗餘。

watch的多種使用方式

watch的寫法有多種,以上案例是最常見的一種方法,接下來介紹所有寫法。

傳值函數

new Vue({
    data: {
        count: 1
    },
    watch: {
        count() {
            console.log('count改變')
        }
    }
})

最常見的寫法,count改變時將會觸發傳值的回調函數

傳值數組

new Vue({
    data: {
        count: 1
    },
    watch: {
        count: [
            () => {
                console.log('count改變')
            },
            () => {
                console.log('count watch2')
            }
        ]
    }
})

傳數組,count改變後會依次執行數組內每一個回調函數

傳值字符串

new Vue({
    data: {
        count: 1
    },
    watch: {
        count: 'handleChange'
    },
    methods: {
        handleChange(val) {
            console.log('count改變了')
        }
    }
})

我們也可以傳值字符串handleChange,然後在methods寫handleChange函數的邏輯,同樣可以做到count改變執行handleChange

傳值對象

new Vue({
    data: {
        count: 1
    },
    watch: {
        count: {
            handler() {
                console.log('count改變')
            }
        }
    }
})

可以傳值對象,該對象包含一個handler函數,當count改變時,會執行此handler函數,爲什麼多此一舉需要包裝一層對象呢?存在即合理,是有其特殊作用的。

傳值對象的其他作用

watch爲監聽屬性的變化,調用回調函數,因此,在初始化時,並不會觸發,在初始化後屬性改變才觸發,如果想要初始時也要觸發watch,那就需要傳值對象,如下:

new Vue({
    data: {
        count: 1
    },
    watch: {
        count: {
            immediate: true, // 加此屬性
            handler() {
                console.log('count改變')
            }
        }
    }
})

傳的對象有immediate屬性爲true,則watch會立刻觸發。

源碼分析watch

本節進行源碼分析,探索watch的真面貌

初始watch

// 初始化
function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.data) {
    initData(vm);
  }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
// 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);
    }
  }
}

從vue的執行流程,讀到了initWatch函數,此函數的用法很清晰,將傳入的每一個watch屬性執行createWatcher處理。如果傳值是數組,則遍歷去調用。

下面看一下createWatcher函數

function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  // 如果是對象,則處理
  if (isPlainObject(handler)) {
    // 將對象緩存,給$watch函數
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}

createWatcher中做了兼容處理:

  1. 如果handler是個對象,則進行一步轉換;
  2. 如果handler是字符串,則取vue實例的方法(methods裏聲明)
  3. 最後調用實例的$watch方法

創建Watcher


Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    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) {
      cb.call(vm, watcher.value);
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };

vm.$watch裏是最終實現watch的部分,在這裏仍然做了兼容判斷,如果是對象,回調createWatcher;接下來就最重要的new Watcher。

$watch的功能其實就是new了一個Watcher,那麼,我們在代碼裏實現的一切響應,都來自於Watcher,下面看一下watch裏的Watcher

watchWatcher

Watcher是vue數據驅動核心部分的一員,他承載着依賴收集與事件的觸發。下面重點解讀一下watch的Watcher實現。

if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    // parsePath去解析expOrFn並返回getter函數
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
    }
  }

watch Watcher會執行上面部分,parsePath源碼可自行查看,他會將obj.a這種寫法兼容, 最終是返回需要監聽的屬性的getter函數

if (this.computed) {
    this.value = undefined;
    this.dep = new Dep();
  } else {
    // 執行get方法
    this.value = this.get();
  }

拿到getter後,會執行this.get方法:

Watcher.prototype.get = function get () {
  // 加入Dep.target
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm); // 執行
  } catch (e) {
  } finally {
    popTarget();
    this.cleanupDeps();
  }
  return value
};

以上爲get方法,內容簡單,但是做的事情舉足輕重,他不僅做了值的獲取,還做了依賴收集。

pushTarget會將當前watchWatcher賦值到Dep.target中,然後執行this.getter函數,要監聽的屬性如count會觸發他的get鉤子,與此同時會進行收集依賴,收集到的依賴就是前面Dep.target也就是當前的watchWatcher

正因爲有上面的依賴收集,使count屬性有了此watchWatcher的依賴,當this.count改變時,會觸發set鉤子,進行事件分發,從而執行回調函數

Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
  var value = this.get();
  if (
    value !== this.value ||
    isObject(value) ||
    this.deep
  ) {
    // set new value
    var oldValue = this.value;
    this.value = value;
    this.dirty = false;
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue);
      } catch (e) {
        handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
      }
    } else {
      cb.call(this.vm, value, oldValue);
    }
  }
};

上面就是this.count改變時,最終調用的方法,在這裏會執行this.cb,也就是定義的watch的回調函數,會把value/oldValue傳遞過去

立即執行的watch

前面說到,watch只會在監聽的屬性改變值後纔會觸發回調,在初始化時不會執行回調,如果想要一開始初始化就執行回調,需要傳參對象,並immediate爲true,實現原理已經在創建Watcher貼出來了

if (options.immediate) {
      cb.call(vm, watcher.value);
}

創建watcher時,如果immediate爲真值,會直接執行回調函數

與computed比較

computed是計算屬性,watch是監聽屬性變化,有些場景計算屬性做的事情,watch也可以做,當然要儘量用computed去做,爲什麼?

new Vue({
    data: {
        num: 1,
        sum: 2
    },
    watch: {
        num(val) {
            this.sum = val +  1;
        }
    }
})

watch實現需要聲明2個data屬性num 和 sum,2個都會加入數據驅動,當num改變後,num和sum都觸發了set鉤子。
而computed不會,computed只會觸發num的set鉤子,因爲sum根本沒有聲明,num改變後是動態計算出來的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章