最後幾天來聊一下關於生命週期的事情,這裡提到的生命週期並不限於 Vue 所屬的區域,也包含了一些原生 JavaScript 那種無關 Bug 而是一種 Feature 的那些 生命週期 的事情。

沒有被 JS 婊過就不算寫過 JS 你說是吧( 並沒有這種說法好嗎


關於封裝與執行

我之前犯過幾個低級錯誤,舉個例子給大家看個笑話:

<template>
    <section class="hello">
        <h1 v-if="window.location.href === '/'">Homepage</h1>
        <h1 v-else>Other pages</h1>
    </section>
</template>

以上是錯誤的範例,至於看不出來哪裡有錯的人 左轉不送謝謝。就字面上來說每個字都對,寫法也沒有錯誤,不過,就 Vue 而言,你會獲得一個 windowundefined 的結果。至於原因嘛,在 Vue 樣版作用域當中,裡面所有的變數是 明確指向該物件 的作用域裡面,也就是說,最終都會指回該物件的 this 本身。

所以,那上面的 window 並不會是你以為的 window 物件。

另外一個關於封裝的低級錯誤:

let myOptions = {}

export default {
  name: 'component',
  data () {
    return {
      options: myOptions
    }
  }
}

以上的寫法也沒有任何錯誤、或是會讓人覺得奇怪的地方。但,錯誤會發生在 這個物件被重用( Reuse )的時候 。我在 第 4 天 有提到會被污染的事情,另外在 第 12 天 也提到了一些生命週期的奇妙狀況。

對於 Webpack 最終封裝的結果來說,你只要是放在 export default {} 外面的事情,最好都可以當他是一個 小規模的全域變數 來看待。如果還是不知道我在講什麼,為什麼這樣會有錯的人,請複習一下 Kuro 的文章:

JavaScript 是「傳值」或「傳址」

歐,你不要以為剛剛的 let myOptions = {} 改成 const myOptions = {} 就會沒事,如果認為會沒事的人 一樣左轉不送謝謝 ,你可以參考一下 Object.freeze() 的相關文件。

犯蠢的事情很多,不過就一些小地方來說,還是得自己留意一下才行。通常不會是因為你 寫法 上面的錯誤,多數狀況是 那些東西寫在哪裡 或是說被 應用在哪裡 的問題。終究你還是得摸清楚你所使用的框架,是不是有什麼需要留意的地方。


那些 XX 週期

終究你可能會學到一件事情,就是那些讓人煩躁的生命週期,最終都是用來浪費生命的(欸)。我舉一個有趣的例子,是關於生命週期與原生 JS 混合的案例:

<template>
    <section>
        <img class="previewer" :src="previewSrc" />
        <input type="file" @change.prevent="previewFile($event)" />
    </section>
</template>
export default {
  name: 'component',
  data () {
    return {
      previewSrc: ''
    }
  },
  methods: {
    previewFile (evt) {
      let fileReader = new window.FileReader()
      fileReader.readAsDataURL(evt.target.files[0])
      fileReader.onload = event => {
        const preview = this.$el.querySelector('.previewer')
        preview.addEventListener('load', () => {
          // 中間就不贅述了。
        }, false)
        
        this.previewSrc = event.target.result
      }
    }
  }
}

上述的例子混合了 Vue 的方法與在原生 JavaScript 操作的 onload 方法。但為何會說這樣的方式會出問題呢?原因在於 前端渲染 這件事,倘若你的操作並不會 重繪 所有的 DOM 結構樹的話,那麼,對於瀏覽器與 JavaScript 來說,那些 DOM 物件與其被綁定的事件,就依舊會在同樣的 位置 上。

這裡的 位置 也包含了記憶體位址。但是,請注意上面例子,同樣都是使用 讀取 的事件,有一個是使用 onload 來指定,而另外一個則使用 addEventListener 來指定,這會造成一些意想不到的錯誤。

那麼,當你重複的操作剛剛的檔案上傳動作時,會有以下這樣的結果:

操作 3 次選擇檔的動作,然後你會發現 Preview 的事件被累加了,而 FileReader 的卻維持正常。主要的差異在於,雖然都是屬於匿名函式,但是 addEventListener 所接受的匿名函式,在每次運作的時候,都會是 新的位置 ,而對於 onload 來說,他只是原有物件屬性,被做了一個 賦值 的動作,所以,兩者所參考的記憶體位置是不同的。

所以,不是每個地方都雞婆的用上 addEventListener 就自以為很會寫 JS 好像很潮。

上述的狀況,如果再加上 updated 這個生命週期來看,那麼,他理所當然的會被夾在 FileReader 與 Preview 中間。不過,倘若你的圖片 src 是用 preview.src 來賦予值的話,那麼你的 updated 不會被呼叫是很合理的,對於 Vue 來說,你自己操作 DOM 跟他無關,所以只要是虛擬節點沒有任何改變,自然就不會呼叫 updated


小結

其實 JavaScript 寫到最後都會懷疑人生,這是一個階段不用太灰心(欸)。

ITHome 鐵人賽同步刊登 Vanilla JS 與 Vue 的生命週期 Day 28