元件裏的 data 必須是函式是為了確保元件裏的資料不會被別的元件資料所污染。如果 data 是物件,因為 JavaScript 的物件是傳址,一旦有元件的資料被修改,別的元件的資料也會被修改。因此,需要用函式,回傳一個新物件的做法,確保自己元件的資料自己改,不會被污染。
另外,如果使用箭頭函式建立 data,只要 data 物件裏沒有用到 this
,就沒有問題。因為這裏的 this
不會指向 Vue ,而是 Window 物件,因此如果你打算使用 this
來取得 Vue 裏的資料的話就會出錯。
以下會作出詳細解說。
平常我們習慣在元件建立 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 裏面如果沒有用到 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 的資料裏,使用 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')
結果 str
是 undefined
。即使是使用箭頭函式還是這裏用到的傳統函式,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
寫 Vue 時我們都習慣使用 this
就能取得在 Vue 的資料,包括 data
、computed
以及呼叫在 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
的概念,如果把 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
所指向的值。在全域時,就會指向 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
不會有 lastName
和 firstName
,所以結果就是 undefined
。
補充一點,以上提到箭頭函式裏的 this
需要往上層找函式,更準確的說法是,需要往上層找作用域,而函式會建立一個作用域,但物件不能。因此,在 greeting
裏的 this
往上一層找是 obj
,但 obj
不會建立一個作用域,最後導致找到全域,並指向 Window
物件。
去年我的鐵人賽系列,JavaScript 基本功修煉,有關於箭頭函式和 this 的文章,有興趣的話也歡迎再參考看看。
this
,就可以使用箭頭函式來建立 data。否則,會因為this
指向 Window 物件,而非 Vue 物件而造成取值時出錯。重新認識 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