[IT 鐵人賽] 題外話:原始碼之藏在 $watch 當中的神奇設定 Day 16

其實,這是我之前寫過的 一篇文章我絕對不會承認是拿來墊檔的)。雖然我覺得這件事情很奇妙,但是比起 EventBus 的都市傳說,這個應該算還行。只要你看過原始碼應該就能理解。

是說,有誰會沒事去挖人家的原始碼來看( 就你啊 )。


再看一次 $watch

我們平常在操作 $watch 的時候,會有兩種寫法:

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18
    }
  },
  watch: {
    myAge (newAge, oldAge) {
      // newAge 表示改變後的新值
      // oldAge 表示改變前的舊值
    }
  }
}

或是你寫在其他的生命週期勾子裡,

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18
    }
  },
  created () {
    this.$watch(() => {
      return this.myAge
    }, (newAge, oldAge) => {
      // newAge 表示改變後的新值
      // oldAge 表示改變前的舊值
    })
  }
}

兩種寫法的差異在於呼叫的時機點不同,一個是在特定的生命週期勾子裡才去 監看 你所想要的變數,另一個是在元件初始化的同時 順便 幫你把想要監看的變數初始化。而第二種寫法,彈性比較大的地方在於,你可以將第一個變數傳入函式回傳值,這中間你想要回傳什麼就都沒差了,甚至你想要做非同步處理也是可以。

當然,你可以監看一個變數,那我們要怎麼解除這個監看呢?根據官方的說法,解除監看( unwatch )的方式,就是呼叫 $watch 所回傳的函式即可。那麼,你就會發現,只有第二種方法可以解除監看這件事情,亦即:

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18,
      myWatcher: void 0
    }
  },
  created () {
    this.myWatcher = this.$watch(() => {
      return this.myAge
    }, (newAge, oldAge) => {
      // newAge 表示改變後的新值
      // oldAge 表示改變前的舊值
    })
  },
  beforeDestroy () {
    // 呼叫剛剛 $watch 回傳的函式,即可解除監看動作
    this.myWatcher()
  }
}

官方說明 $watch 的段落


接著,我們來提一下官方文件所沒有提到的部分。上述的段落是比較常見的作法,其實 $watch 還是有比較奇妙的寫法。首先,第三個參數在官方文件也有提及,

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18,
      myWatcher: void 0
    }
  },
  created () {
    this.myWatcher = this.$watch(() => {
      return this.myAge
    }, (newAge, oldAge) => {
    }, {
      // 這邊可以放第三個參數
      immediate: true
    })
  },
  beforeDestroy () {
    this.myWatcher()
  }
}

第三個參數可以接受的設定有:

  • immediate 即刻救援 即刻執行。
  • deep 深度觀察。
  • user 呼叫執行的保護動作。
  • lazy 初始化執行動作,以及是否使用延遲監看(預設是 true)。
  • sync 同步處理,與 lazy 互斥,但 lazy 優先。

以上除了 immediatedeep 官方文件有提到以外,剩下三件事情官方沒有多加著墨,如果想要稍微理解的話,可以看我之前 $watch 的文章

以我們目前鐵人賽所使用的 Vue 2.6.10 的版本來說,這個 $watch 的原始碼可能已經跟我原本的文章有一點出入,不過基本上操作邏輯還是相同。

那麼,除了上述在勾子裡使用 $watch,如果元件屬性 watch 要使用的話,官方也是沒有告訴你該怎麼使用,我們來看看這個例子:

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18
    }
  },
  watch: {
    myAge: {
      handler (newAge, oldAge) {
        // 這邊就是原本的回呼函式
      },
      // 以下就是第三個參數
      immediate: true,
      deep: true
    }
  }
}

以上就是目前 $watch 可以使用的所有方法。只是,如果你想要能夠取消,那你就只能使用在勾子裡面操作的那種方式,寫在元件裡面的這種 watch 在元件被銷毀之前,是沒辦法取消的。


再看生命週期

扣除掉你在生命週期的勾子當中所建立的 $watch 之外,元件本身的 watch 到底是存在於生命週期的那個部分?我們簡易的寫一個範例來運作,看看結果為何:

我們這邊簡單的利用四種生命週期,來追查 watch 到底會出現在哪裡:

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted

我在 created 這個生命週期的勾子當中,執行了 this.age = 99 這個動作。所以我們看上面執行結果的例子,你會發現 age watcher 出現在 mounted 之後。由於我們要在 created 之後才會有 this 這個實例,所以當我們執行了 this.age = 99 之後,觸發了 watch 的監看,最終在 mounted 才會有反應。

無論你多早呼叫,最後會在 mounted 才會有反應。

但是,我們來看看第三個參數 immediate 的例子:

你會發現在上面的執行結果當中,出現了兩次 age watcher 的訊息,原因在於,我們在 watch 的第三個參數 immediate: true,所以,我們執行結果就會有兩次 age watcher 訊息。

那麼,第一次的 age watcher 訊息,發生在 beforeCreatecreated 中間,這個地方就是元件的 watch 被初始化的地方。而在 mounted 之後,就是當我在 created 呼叫的時候,他被 觸發 的反應結果會是在 mounted 之後才發生。

當你使用了 immediate: true,那麼元件建立完成之前就會先被呼叫一次。

根據這樣的結果,我們可以知道,其實 watch 被初始化的地方,跟元件本身是一樣的。所以,如果你加上了 immediate: true 的參數,那麼你就必須注意他在初始化就會被呼叫。

另外,在 watch 還有一件事情,無論你的第三參數為何,倘若你所 監看 的變數,並不會影響到渲染結果,那麼,在生命週期當中,關於元件的更新方法 beforeUpdateupdated 就不會被呼叫,亦即,只有 watch 本身的 handler 會被執行而已。


隱藏函式 before

在目前版本 Vue 2.6.10 當中,我們的 Watcher 第三個參數中,還隱藏了一個 before 的屬性可以使用,他本身是接受一個函式的呼叫,觸發的時機點是在你的 watch 被觸發之前:

export default {
  name: 'Component',
  data () {
    return {
      myAge: 18
    }
  },
  watch: {
    myAge: {
      handler (newAge, oldAge) {
        // 這邊就是原本的回呼函式
      },
      // before 函式
      before () {
        console.log('age watcher before function')
      }
    }
  }
}

在上述的執行結果當中,你會看到 age watcher before function 的結果被印出,這個函式到底是做什麼用的呢?其實,這個 before 最原始的用意,是 Vue 自身對於 Component 的某些生命週期的勾子,例如 beforeUpdate,他也是使用 Watcher 去監看的。所以,他其實並非對外開放的方法。

具體來說,這個 before 會在 handler 被呼叫 之前 被執行,所以,基本上你可以在這邊做一些相對應的邏輯操作,或者,去干涉你原本的 handler但是請不要做這種蠢事 )。這個函式所回傳的 this 並不會是你的元件,而是這個元件內部的 Watcher 物件:

這個 Watcher 物件就是元件本身所包含的監看動作,每一種不同的 watch 對象,都會有一組對應的 Watcher 物件。如果你不確定你自己在做什麼,或者是酒過三巡之後,請不要任意對這個物件做操作。由於這個物件並 不是 不可修改( Immutable ),所以你若是硬要從這邊干涉你的對象元件,或是 handler 的操作,是真的會運作的。

由上述的例子你會發現,你原本的 handler 被你自己改掉了。再次聲明,他 不是對外開放的方法,除非你自己知道在做什麼事情,不然請不要任意覆寫 Watcher 這個物件。


小結

希望這個章節能夠讓你比較瞭解 watch 的作用方式,當你下次使用 watch 的時候,應該就能理解觸發時機、觸發結果,以及為什麼沒有被觸發了。

最後再嘴一下 $watch 藏在原始碼裡的邊緣人 ,下次遇到 watch 不會動,可以參考這一篇。

ITHome 鐵人賽同步刊登 題外話:原始碼之藏在 $watch 當中的神奇設定 Day 16

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei