iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
3
Modern Web

勇者鬥Vue龍系列 第 8

Vue.js Core 30天屠龍記(第8天): 監聽器( watch )

Vue 提供了監聽器,當資料變化時叫用函數,函數會有兩個傳入參數: 改變前的值、改變後的值,可以使用這個函數做跟此資料變化有關的處理

介紹

監聽器在 Vue.js 中有兩種使用方式:

  • $watch : 實體上的函數,使用此函數註冊監聽器。
  • watch : 實體上的屬性,此屬性設置的物件在實體建立時會叫用 $watch 註冊監聽器。

$watch 是註冊監聽器的函數,而 watch 是為了開發者方便在實體上設置監聽器而提供的,其實 watch 本身也是使用 $watch 註冊監聽器。

接下來讓我們來看看如何使用這兩種方式設置監聽器。

$watch

定義

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

回傳值

$watch 的回傳值是註銷監聽器的函數,執行此函數可使監聽器失效。

參數說明

  • expOrFn : 設定要監聽的目標,可以使用 JavaScript 表達式或是一個回傳監聽目標值的函數
  • callback : 當數值改變時要叫用的函數,此函數會有兩個傳入參數: callback(newVal, oldVal)
    • newVal : 改變後的資料值。
    • oldVal : 改變前的資料值。
  • [options] : 非必要參數,監聽器的設定。
    • deep : 監聽物件時,物件的下層屬性變化也會觸發監聽器。
    • immediate : 在實體初始化設置監聽器的時候會馬上叫用 callback 函數。

expOrFn 參數中使用的 JavaScript 表達式只能是以逗點分隔的物件路徑,像是a.b.c,如果需要監聽更複雜的表達式可以使用函數。

callback 函數中如果要使用 this ,則不能使用 arrow funciton 。

範例

基本

var vm = new Vue({
  ...
  data: {
    a: 1,
    newA: 0,
    oldA: 0
  }
});

vm.$watch('a', function(newA, oldA) {
  this.newA = newA;
  this.oldA = oldA;
});
<div id="app">
  <button @click="a++">+</button>
  <button @click="a--">--</button>
  <div>a: {{a}}</div>
  <div>changed: {{newA}}</div>
  <div>before change: {{oldA}}</div>
</div>

按下 + / - 按紐時, $watch 會去更新 newAoldA ,在畫面上會看到 {{newA}} 總會跟 a 相同,而 {{oldA}} 會是 a 改變之前的值。

監聽以 . 分隔的表達式

var vm = new Vue({
  ...
  data: {
    ...
    b: {
      c: {
        d: 1
      }
    },
    newD: 0,
    oldD: 0,
  }
});

...

vm.$watch('b.c.d', function(newD, oldD) {
  this.newD = newD;
  this.oldD = oldD;
});

跟前一個例子相同的功能,只是這次監聽的值在物件裡面,使用表達式 b.c.d 就可以監聽 d 屬性。

監聽物件下的各個屬性值

假設有個需求是只要物件下的其中一個屬性值改變,就要觸發監聽器,為此需要在 $watch 的第三個參數 [options] 加上 deep: true 的設定,讓監聽器知道要監聽下層的屬性。

var vm = new Vue({
  ...
  data: {
    ...
    b: {
      c: {
        d: 1,
        e: 1,
      }
    },
    newD: 0,
    oldD: 0,
    newB: {},
    oldB: {},
  }
});

...

vm.$watch('b', function(newB, oldB) {
  this.newB = newB;
  this.oldB = oldB;
}, {
  deep: true
});
<div id="app">
  ...
  <div>
    <button @click="b.c.d++">+</button>
    <button @click="b.c.d--">-</button>
    <div>b.c.d: {{b.c.d}}</div>
    <button @click="b.c.e++">+</button>
    <button @click="b.c.e--">-</button>
    <div>b.c.d: {{b.c.e}}</div>
    <div>changed b: {{newB}}</div>
    <div>before change b: {{oldB}}</div>
  </div>
</div>

修改 b.c.db.c.e 時也會觸發 b 的監聽器去更新 newBoldB

使用函數設定目標

如果要監聽複雜的表達式,使用函數來設定目標。

var vm = new Vue({
  ...
  data: {
    ...
    f: 1,
    g: 1,
    fPlusg: 0
  }
});

...

vm.$watch(function() {
  return this.f + this.g;
}, function(fPlusg) {
  this.fPlusg = fPlusg;
});
<div id="app">
  ...
  <div>
    <div>
      f: {{f}}
      <button @click="f++">+</button>
      <button @click="f--">-</button>
    </div>
    <div>
      g: {{g}}
      <button @click="g++">+</button>
      <button @click="g--">-</button>
    </div>
    <div>f + g: {{fPlusg}}</div>
  </div>
</div>

上面的例子 f + g 的值不能用只有點分隔的表達式來表示,所以必須使用函數,函數回傳的值改變就會觸發監聽器。

實體初始化時叫用監聽器

常常會有一種情境是在實體初始化完成時要取得資料,而在資料改變時會使初始資料變化,這時我們也許向下面這樣設定:

var vm = new Vue({
  ...
  data: {
    ...
    n: 10,
    zeroToNArr: []
  },
  created() {
    this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
  }
});

...

vm.$watch('n', function() {
  this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
});
<div id="app">
  <div>
    <div>
      n: {{n}}
      <button @click="n++">+</button>
      <button @click="n--">-</button>
    </div>
    zeroToNArr: {{zeroToNArr}}
  </div>
</div>

created 鉤子我們設置 zeroToNArr 初始值,然後監聽 n 變化時重設 zeroToNArr ,這樣看起來有點嘮叨,讓我們使用監聽器的 immediate 選項來減化代碼:

var vm = new Vue({
  ...
  // created() {
  //   // [0, 1, 2, 3..., n]
  //   this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
  // }
});

...

vm.$watch('n', function() {
  // [0, 1, 2, 3..., n]
  this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
}, {
  immediate: true
});

這樣不僅簡潔了許多,在初始實體時也會去設定 zeroToNArr 的初始值了。

註銷監聽器

使用 $watch 回傳的函數就可以註銷監聽器。

var vm = new Vue({
  ...
  data: {
    ...
    n: 10,
    zeroToNArr: [],
    
    unwatchNFunc: () => {}
  }
});

...

vm.unwatchNFunc = vm.$watch('n', function() {
  // [0, 1, 2, 3..., n]
  this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
}, {
  immediate: true
});
<div id="app">
  <div>
    <button @click="unwatchNFunc">unwatch n</button>
    <div>
      n: {{n}}
      <button @click="n++">+</button>
      <button @click="n--">-</button>      
    </div>
    zeroToNArr: {{zeroToNArr}}
  </div>
</div>

這裡將 $watch 的回傳值丟給 unwatchNFunc ,當按下 unwatch n 按鈕時觸發 unwatchNFunc 函數,註銷監聽器。

watch

物件定義

watch: {
  key: value,
  ...
}
  • watch 為鍵值,下面定義的屬性都是欲監聽的資料來源。
  • key : 監聽目標名稱,可以使用 JavaScript 表達式。
  • value : callback 函數的設定,共有 stringFunctionObjectArray 可以設定。
    • string : callback 函數名稱。
    • Function : callback 函數。
    • Object : 設定監聽物件,設定方式如下:
      • handler : callback 函數。
      • deep: 布林值,是否監聽物件下層屬性。
      • immediate: 布林值,是否在實體初始化時立即叫用 callback 函數。
    • Array : 當有多個監聽器時,使用陣列帶入多個 callback 函數。

key 的 JavaScript 表達式跟 $watch 相同,都只能設定以點為分隔的表達式。

value 設定的 callback 函數跟 $watch 相同,有 newValoldVal 兩個傳入參數。

範例

watch 的使用方式大部分跟 $watch 相似,這裡直接以範例說明相似的用法。

var vm = new Vue({
  ...
  watch: {
    // basic
    a: function(newA, oldA) {
      this.newA = newA;
      this.oldA = oldA;
    },
    // string
    a: 'aMethod',
    // expression
    'b.c.d': function(newD, oldD) {
      this.newD = newD;
      this.oldD = oldD;
    },
    // deep
    b: {
      handler: function(newB, oldB) {
        this.newB = newB;
        this.oldB = oldB;
      },
      deep: true
    },
    // immediate
    n: {
      handler: function() {
        // [0, 1, 2, 3..., n]
        this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
      },
      immediate: true
    }
  },
  methods: {
    aMethod(newA, oldA) {
      this.newA = newA;
      this.oldA = oldA;          
    }
  }
});

用字串叫用 callback 函數

可以使用把方法名稱字串設置為 value ,監聽器會去註冊此名稱的方法。

var vm = new Vue({
  ...
  watch: {
    a: 'aMethod',
  },
  methods: {
    aMethod(newA, oldA) {
      this.newA = newA;
      this.oldA = oldA;
    }
  }
});

使用 computed 替代函數目標

如果是複雜的表達式可以使用 computed 算出,再使用 watch 監聽此計算屬性。

var vm = new Vue({
  data: {
    f: 1,
    g: 1,
    fPlusg: 0,
  },
  computed: {
    fPlusgComputed() {
      return this.f + this.g;
    }
  },
  watch: {
    // function
    fPlusgComputed: function(fPlusg) {
      this.fPlusg = fPlusg;
    },
  }
});

同個資料來源註冊多個監聽器

var vm = new Vue({
  ...
  data: {
    ...
    z: 1,
    zminus: 0,
    zplus: 0
  },
  watch: {
    ...
    z: [
      {
        handler: function(newVal) {
          this.zminus = newVal - 1;
        }
      },
      function(newVal) {
        this.zplus = newVal + 1;
      }
    ]
  }
});

value 是陣列時, Vue 會註冊陣列中的每一個監聽器,可以使用不同的定義方式( stringFunctionObjectArray )設定,本例使用 ObjectFunction 舉例。

無法註銷監聽器

使用 watch 無法註銷監聽器,如果要在選項中註銷監聽器可以在 created 中使用 $watch

var vm = new Vue({
  data: {
    n: 10,
    zeroToNArr: [],
    
    unwatchNFunc: () => {},
  },
  created() {
    this.unwatchNFunc = this.$watch('n', function() {
      // [0, 1, 2, 3..., n]
      this.zeroToNArr = Array.from(new Array(this.n + 1), (val, index) => index);
    }, {
      immediate: true
    });
  }
});

Demo

小結

監聽器是監看資料,如果有變化時觸發函數,監聽器有 $watchwatch 兩個的使用方式,本文以不同的範例說明它們如何設置,還有在使用上的差別。

下一章會將監聽器跟計算屬性這兩個極為相似的功能做比較,讓我們對它們的使用時機能夠掌握得更好。

參考資料


上一篇
Vue.js Core 30天屠龍記(第7天): 計算屬性
下一篇
Vue.js Core 30天屠龍記(第9天): 計算屬性與監聽器的比較
系列文
勇者鬥Vue龍32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
brank1214
iT邦新手 5 級 ‧ 2020-07-08 11:23:50

請問大大為何實例中b.c.e變化時的newB & oldB 看起來是一樣的呢
https://ithelp.ithome.com.tw/upload/images/20200708/20128471gnXELnxra1.png
版主提供可運行的程式範例
https://ithelp.ithome.com.tw/upload/images/20200708/201284717Pp2rdNClp.png

你好~

我分為簡答跟詳答回覆你:

簡答

依照 Vue 官方文件 解釋:

Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.

Vue 的操作會使 Object, Array 在新舊值中是對應到相同的記憶體位置,因此新舊值會相等

詳答

我建立了一個範例簡略模擬 Vue watcher 的流程:

https://codepen.io/peterhpchen/pen/bGEMwNB?editors=0012

可以試著將 b 由數值改為物件再跑一次,從中瞭解為什麼會相等

Vue 的 watcher 可以在代碼庫中找到:https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L179

如何避免

利用監聽計算屬性與 Object.assign 複製物件的方式避免等值問題,詳細可以看此 Github issue

brank1214 iT邦新手 5 級 ‧ 2020-07-09 14:25:04 檢舉

謝謝版主抽空回應,簡答好像對我比較好理解XD
詳答的部分可能還需要再更熟悉javaScript知識才能理解= =

0
Detox
iT邦新手 5 級 ‧ 2020-11-22 00:46:05

大大您好
想請問如果用watch監聽vue-router的變化
像這樣

  watch: {
    $route(nowRoute) {
      let nowPage = nowRoute.name;
      this.zoom(nowPage);
    },
  }

目前都是router一變化就會跑zoom了
是否有辦法等到transition結束之後再做呢

可以試試將 zoom 放在 transition 的 hook 中,可以參考 https://vuejs.org/v2/guide/transitions.html#JavaScript-Hooks

我要留言

立即登入留言