iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 9

不只懂 Vue 語法:為何元件裏的 data 必須是函式?建立 data 時能否使用箭頭函式?

問題回答

元件裏的 data 必須是函式是為了確保元件裏的資料不會被別的元件資料所污染。如果 data 是物件,因為 JavaScript 的物件是傳址,一旦有元件的資料被修改,別的元件的資料也會被修改。因此,需要用函式,回傳一個新物件的做法,確保自己元件的資料自己改,不會被污染。

另外,如果使用箭頭函式建立 data,只要 data 物件裏沒有用到 this,就沒有問題。因為這裏的 this 不會指向 Vue ,而是 Window 物件,因此如果你打算使用 this 來取得 Vue 裏的資料的話就會出錯。

以下會作出詳細解說。

建立 data 時需要使用函式

平常我們習慣在元件建立 data 時,使用 function return 的方式,假設有一個名為 <Example /> 的元件,裏面有以下資料:

Example.vue

data(){
    return {
        foo: 1
    }
}

但以下的寫法就會報錯:
Example.vue

data: {
    foo: 1
}

Vue 規定需要使用函式回傳一個新物件是為了避免元件資料互相污染。因為 JavaScript 物件是傳址(pass by reference),因此,當 Example 這元件被重複在多個地方使用時,一旦其中一個元件的資料被修改,其他元件的資料也會一併被修改掉。例如我有 4 個 Example 元件,只要其中一個 Example 元件的資料被修改,其他 Example 元件也會受影響:

<Example />
<Example />
<Example />
<Example />

這個示範模擬了共用同一個物件作為元件資料的情況。

官方文件這裏也有相關的示範例子。

注意: 從 Vue 3 開始,不論是根元件或子元件,都必須使用 function return,否則會報錯。而在 Vue 2 則容許在根元件直接使用物件,但子元件仍然必須使用 function return。

建立 data 時,能否用箭頭函式?

在建立 data 資料時,data 裏面如果沒有用到 this,就能放心使用箭頭函式。原因是箭頭函式的 this 會指向 Window 物件,不是 Vue 物件。

這個例子示範了以上提到的情況。

使用箭頭函式:

const app = Vue.createApp({})

app.component('Message', {
  template: `
  <p> 目前 this 指向的物件:{{ thisObj }} </p>
  <p> 結果:{{ str }} </p>
  `,
  props: {
    msg: {
      type: String
    }
  },
  data: () => ({
      str: this.msg,
      thisObj: this // Window 物件
  })
})

app.mount('#app')

結果:

結果沒顯示到 str,但使用 Vue 檢查工具時會發現,str 是 undefined:

因為 Window 不會有 str 屬性,因此是 undefined
使用傳統函式看看:

data() {
    return {
      str: this.msg,
      thisObj: JSON.stringify(this) // Vue 的 data 物件
    }
}

這裏使用 JSON.stringify 把 Vue 的物件顯示出來,否則會報錯。

結果:

查看 Vue 檢查工具:

因此,使用箭頭函式是沒問題。但如果 data 裏有用到 this,就會出錯。因為 this 會指向 Window 物件,而非 Vue 的物件,因此無法正確取到在 Vue 所建立的資料。

在 data 裏使用 computed 資料?

在複習此題目時,想起能不能在 data 的資料裏,使用 this 來取得 computed 裏的資料。雖然這個做法沒必要,因為 computed 裏的資料本身是函式,它會回傳一個值,所以平常我們只需要直接取 computed 的值來用即可。像是這樣:

<p> {{ addSomeText }} </p>
computed: {
    addSomeText() {
        return 'Add some text'
    }
}

結果畫面就會顯示 "Add some text"。

雖然沒必要在 data 取得 computed 的資料,但還是想試試,如果在 data 裏取得 computed 裏的值會怎樣:

<div id="app">
  <Message job="Web developer" />
</div>
const app = Vue.createApp({})

app.component('Message', {
    template: 
        `<p> {{ str }} </p>`
    ,
    props: {
      job: {
        type: String
      }
    },
    data(){
      return{
        str: this.addSomeText,
        name: 'Alysa'
      }
    },
    computed: {
      addSomeText() {
        return 'Add some text'
      }
    }
    })
    
app.mount('#app')

結果 strundefined。即使是使用箭頭函式還是這裏用到的傳統函式,str 的值都一樣是 undefined

原因不在於 data 使用箭頭函式與否,而是生命週期的問題。因為 Vue 會先建立 data 資料,之後才建立 computed 的資料,因此在 data 裏無法取到 addSomeText 的值,因為 addSomeText 在 data 建立時是 undefined

試試以下例子,就會發現 str 能成功取到值:

HTML:

<div id="app">
  <Message job="Web developer" />
</div>

JavaScript:

const app = Vue.createApp({})

app.component('Message', {
        //在畫面呼叫 str 函式
        template: 
          `<p> {{ str() }} </p>`
        ,
        props: {
          job: {
            type: String
          }
        },
        data(){
          return {
            // 把 str 改為函式,回傳 this.addSomeText
            str() {
              return this.addSomeText
            },
            name: 'Alysa'
          }
        },
        computed: {
          addSomeText() {
            return 'Add some text'
          }
    }
})

app.mount('#app')

這是因為我們在 template 裏呼叫 str 函式,意思就是當整個畫面都被渲染好後,才會呼叫 str,這時候就一定能取到 computed 裏的資料。

題外話,在這情況下,我們用箭頭函式建立 data 也會成功取到值,但 str 必須要是傳統函式:

JavaScript:

data: () => ({
    str() { 
      return this.addSomeText
    },
    // 以下會回傳 undefined
    // str: () => this.addSomeText, 
    name: 'Alysa'
}),

原因是我們在畫面中,在 {{ }} 裏呼叫 str。這兩個大括號是指向 Vue 實體物件裏的狀態,所以事實上我們是透過 Vue 物件來呼叫 str,因此 str 裏的 this 會指向 Vue。

關於 this 的運作,此文章的最後部分會再簡單重溫一遍。

完整程式碼示範

https://codepen.io/alysachan/pen/jOwZRNa?editors=1011

為什麼使用 this 可以取到 Vue 裏的資料?

寫 Vue 時我們都習慣使用 this 就能取得在 Vue 的資料,包括 datacomputed 以及呼叫在 methods 建立的方法等等。

HTML 的部分:

<div id="app">
  <User job="Web developer" />
</div>

Vue 的部分:

const app = Vue.createApp({})
app.component('User', {
  props: {
    job: {
      type: String
    }
  },
  template: `<p> {{ fullName }} </p>`,
  data: () => ({
      firstName: 'Alysa',
      lastName: 'Chan'
  }),
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  },
  methods: {
    greeting(){
      console.log(`Hi I'm ${this.fullName}`)
    }
  },
  mounted() {
    console.log(this) // Proxy 物件
    this.greeting() // Hi I'm Alysa Chan
  }
})
app.mount('#app')

這時候查看 console 會發現一個由 Vue 包裝好的 Proxy 物件:

由此可見,Vue 會把所有建立的資料和函式全都放在同一個物件上,並且成為 Proxy 代理的 target。在以上例子中,我在 methods 裏使用 this.fullName來引用在 computed 裏的 fullName。該 this 會指向這個 target 物件裏的 fullName

因此,以上情況就等於以下的寫法:

const obj = {
    firstName: 'Chan',
    lastName: 'Alysa',
    fullName() {
        return `${this.lastName} ${this.firstName}`
    },
    greeting(){
        console.log(`Hi I'm ${this.fullName()}`)
    }
}

obj.greeting() // Hi I'm Alysa Chan

補充一點,Vue 是使用 Proxy 來實現響應式更新(Reactivity)。我在前幾天的文章有討論過,有興趣的話歡迎看看。

簡單重溫 this 的概念

這裏用以上例子,稍為重溫 this 的概念,如果把 fullName 改為箭頭函式,會出現什麼結果?

const obj = {
    firstName: 'Chan',
    lastName: 'Alysa',
    fullName: () =>`${this.lastName} ${this.firstName}`,
    greeting(){
        console.log(`Hi I'm ${this.fullName()}`)
    }
}

obj.greeting()

結果 console 會出現:Hi I'm undefined undefined

在回答這問題之前,要先知道幾個核心概念:

  • this 只存在於函式裏。
  • this 的值是取決於怎樣呼叫這函式。
  • 傳統函式裏 this 的值,會指向你呼叫此函式時,所引用的物件。
  • 箭頭函式裏 this 的值,會繼承上一層函式 this 所指向的值。在全域時,就會指向 Window 物件。

最後兩點可能比較難理解,但套用到題目裏就更清晰了。

題目中,第一步是用 obj.greeting() 來呼叫 greeting 函式。

greeting 是使用傳統函式。因此,在 greeting 裏的 this.fullName,這個 this 會指向 obj 這物件。上面提過,傳統函式裏 this 的值,會指向你呼叫此函式時,所引用的物件。在這裏,我是用 obj 來呼叫 greeting,所以 greeting 裏的 this 會指向 obj

第二步,就是在 greeting 裏呼叫 this.fullName(),因為之前提到,這裏的 this 是指向 obj,所以意思就是 obj.fullName(),換言之,即是呼叫在 obj裏的 fullName 函式。

但是,fullName 是使用箭頭函式。 雖然使用 obj.fullName() 來呼叫fullName,但在 fullName 裏的 this不會指向 obj。箭頭函式裏的 this 會往上一層找,看看有沒有函式,以及這個函式所指向的 this 是什麼。 但目前 fullName 再往上層找,只有 obj 這物件,並沒有函式。直至找到全域,並指向 Window 物件。因為 Window 不會有 lastNamefirstName,所以結果就是 undefined

補充一點,以上提到箭頭函式裏的 this 需要往上層找函式,更準確的說法是,需要往上層找作用域,而函式會建立一個作用域,但物件不能。因此,在 greeting 裏的 this 往上一層找是 obj,但 obj 不會建立一個作用域,最後導致找到全域,並指向 Window 物件。

去年我的鐵人賽系列,JavaScript 基本功修煉,有關於箭頭函式this 的文章,有興趣的話也歡迎再參考看看。

總結

  • 建立 data 時,需要用函式,把 data 資料放在此新物件裏,並回傳出來。原因是避免因為 JavaScript 的物件傳址的特性,造成元件之間的資料互相污染。
  • 只要 data 裏沒有用到 this,就可以使用箭頭函式來建立 data。否則,會因為this指向 Window 物件,而非 Vue 物件而造成取值時出錯。
  • Vue 會先建立 data 資料,之後才會建立 computed 資料。
  • 傳統函式裏 this 的值,會指向你呼叫此函式時所引用的物件。箭頭函式裏 this 的值,會繼承上一層函式 this 所指向的值。在全域時,就會指向 Window 物件。

參考資料

重新認識 Vue.js - 1-2 Vue.js 的核心: 實體
重新認識 Vue.js - 2-1 元件系統的特性
Vue JS: Difference of data() { return {} } vs data:() => ({ })
Use computed property in data in Vuejs


上一篇
不只懂 Vue 語法:請說明 style 裏的 scoped、deep selector 的作用?
下一篇
不只懂 Vue 語法:什麼是單向資料流和雙向綁定?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31

尚未有邦友留言

立即登入留言