Vue 組件 data 爲什麼必須是函數
文章目錄
教科書般的解釋(官網原話)
當一個組件被定義,data 必須聲明爲返回一個初始數據對象的函數,因爲組件可能被用來創建多個實例。如果 data 仍然是一個純粹的對象,則所有的實例將共享引用同一個數據對象!通過提供 data 函數,每次創建一個新實例後,我們能夠調用 data 函數,從而返回初始數據的一個全新副本數據對象
👆 注意不要囫圇吞棗,感受下下面 2 句話:
- 是
組件
爲什麼必須是函數 - 組件可被創建多個實例
很長一段時間我都理解爲:
爲什麼 vue 的 data 需要函數返回,那我們直接引入 JS 使用的時候,new Vue
也沒見的一定要函數返回啊。直到今天才發現是理解少了幾個字,vue 創建的組件的 data 才需要函數返回
不想看分析的直接看這裏
new Vue
可以不使用函數返回的原因在於,每次new
的時候,傳入的都是新的對象(新的內存地址)。所以修改其中一個 vue 實例並不會影響其他實例
對於組件而言,組件定義好之後是有默認值 (我們把一個組件引入後,修改了部分值後。再次引入相同的組件時,第二次引入的組件初始值還是保持原來設置的) 所以在組件註冊(vue 的一個內部流程)的時候,vue 會把這個組件傳入的配置存下來,多次生成同一個組件的時候都會從存下來的配置中取值,然後通過
new
創建新的組件實例。可如果這時候 data 爲對象 (引用類型的內存地址是一樣的) ,那每次生成新的組件實例的 data 都指向了同一個內存區域,這時候其中一個同類型組件值更新了。其餘的都會跟着一起更新
要解決上述說的組件的問題,就需要用函數的形式,每次創建組件都通過 function 返回一個新的對象(內存地址不一樣的對象)。這樣組件的 data 纔是自己單獨的
要理解這個問題,得從原型說起
不熟看這裏 👉 原型和原型鏈-基礎,但是非常重要
3 個栗子 理解後在看源碼
1. 案例 1:
function Animal() {}
Animal.prototype.data = { name: '寵物店', address: '廣州' }
var dog = new Animal()
var cat = new Animal()
console.log(dog.data.address) // 廣州
console.log(cat.data.address) // 廣州
dog.data.address = '東莞'
console.log(cat.data.address) // 東莞
dog.data === cat.data // true
第一個小結論
dog 和 cat 的原型都是 Animal。自然會繼承原型的屬性。繼承過來後,
因爲 data 是普通對象
,屬於引用數據類型
,所以 dog 和 cat 的 data 其實都指向同一塊內存地址就連嚴格運算符判斷都是相等的,說明他們值相等,內存地址也相同,修改其中一個將會影響另外一個
2. 案例 2:
function Animal() {
this.data = this.data()
}
Animal.prototype.data = function() {
return { name: '寵物店', address: '廣州' }
}
var dog = new Animal()
var cat = new Animal()
console.log(dog.data.address) // 廣州
console.log(cat.data.address) // 廣州
dog.data.address = '東莞'
console.log(cat.data.address) // 廣州
console.log(dog.data.address) // 東莞
dog.data === cat.data // false
稍微解釋下:爲什麼第二行:this.data = this.data()
我們在執行 new 的過程中,Animal 其實充當了
constructor
。詳情可以看 new 一個對象發生了什麼。這時候this.data
還是一個函數,還沒執行的函數,所以調用一下 this.data()。讓函數返回一個值。然後重新賦值給this.data
結論 2
用了 function 後,data 都被鎖定在當前 function 的作用域中,然後被返回出去,相當於創建了另外一個對象,所以多個實例之間不會相互影響
3. 案例 3
function Animal({ data }) {
this.data = data
}
var dog = new Animal({ data: { name: '寵物店', address: '廣州' } })
var cat = new Animal({ data: { name: '寵物店', address: '廣州' } })
console.log(dog.data.address) // 廣州
console.log(cat.data.address) // 廣州
dog.data === cat.data // false
結論 3
注意這裏的變量聲明方式,是直接放在了構造函數中,並不是通過原型鏈來查找的。這也就是爲什麼
new Vue
的時候 data 可以爲非函數,在構造函數執行的時候,data 就已經相互隔離
使用 debugger,看下 new vue 發生了什麼
多圖預警!! new Vue 發生了什麼!!
關於 new Vue
,可以看案例 3
。在 new 的過程中,就已經傳入參數賦值
開始 debugger
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
debugger
// 在 new Vue之前,進入debugger模式
var app = new Vue({
el: '#app',
data: { message: 'Hello Vue!' }
})
1. 走到了初始化 vue 的步驟
2. 來到 init 方法內部
- 4994 行 我們常見的 vm 對象。其實就是 vue 的 this 對象。(圖片截的不夠長,往上一點能看到 vm = this)
- 4998 我們常說的生命週期第一步
beforeCreate
- 5000 這是我們今天要深究的函數:
initState
初始化 data 對象的 - 5002 生命週期第二步
create
驗證了 vue 生命週期的一個知識點:beforeCreate 還不能拿到 this.data。需要在 create 的時候才能拿到
3. 來到 initState 方法
- 可以看到初始化
props
、初始化methods
。然後纔到初始化data
。如果沒有 data 還會給個默認值{}
- 初始化
data
後開始處理computed
。然後掛載watch
- 主題是研究
data
。繼續進入到initData
函數裏面
4. initData 方法
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
- 可以看到是有判斷,如果傳入的是函數,就調用該函數(
getData方法裏面就是調用函數返回對象的
)。如果不是函數就默認拿 data,否則還是個默認值。 - 接下來的步驟就是開始做一些代理,數據挾持的監聽
proxy
、observer
之類的不在我們 data 討論範疇了。下次在分析
小結
new vue 小結
new Vue 的過程和案例 3 是非常相似的,只是單純的傳入對象,然後使用 new 的特性,給
vm._data
對象賦值,其實也就是爲當前的 vue 實例的 data 賦值,由於 new 的特性在,所以 data 不強求函數返回,當然也可以函數返回
new Vue 的源碼簡單的看下。那繼續看今天主角 components
的實現
components
作爲一個組件類型,只是一個簡單的工廠模式(一開始的組件參數都是定好的,需要就創建一個新的組件,簡稱工廠模式),創建很多的組件實例。就像案例 1
一樣
還是先寫一個 debugger 進入源碼
日常多圖預警!!
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
// 在 new Vue之前,進入debugger模式
debugger
// 定義一個名爲 button-counter 的新組件
Vue.component('button-counter', {
data: function() {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
1. 進入到了 initAssetRegisters 初始化登記註冊(組件註冊)
- 5225 行。判斷要註冊的是一個
component
組件 - 5232 行。判斷要註冊的是
directive
指令。註冊事件都的確在initAssetRegisters
中。 - 5226 行
validateComponentName
驗證組件名稱是否被佔用 - 5229 是判斷組件是否有定義的名稱,沒有就用自己組件的標籤。這是爲了 上一步,驗證組件是否已經生成的。
- 5230
this.options._base.extend(definition)
有這麼一段代碼,下一步就到這裏面看看
2. extend 函數中
this.options._base
其實就是下圖中的 Vue。調用Vue.extend
- 留意看 5146-5148 行。我在 5147 打了斷點。後續的步驟會回到這裏
- 這一路執行下來。生成了一個
Sub
對象。 - 5149-5155 行。就是準備一個
new
的過程。
Sub.prototype = Object.create(Super.prototype) // 構造器原型
Sub.prototype.constructor = Sub // 構造函數等於Sub方法。在new的時候就會執行Sub裏面的內容
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions) // 合併參數等
- 5192 行。把
Sub
對象 return 了回去。那就是回到了initAssetRegisters
函數那邊去了 - 回去後,把
Sub
賦值給了definition
對象(第一步的 5230 行) - 接着 definition 也被返回出去了。其中這一個返回被一個函數包裹着。函數被賦值爲
Vue[type]
(第一步的 5217 行接收了)這時候 type 是component
。相當於 調用Vue.component
的話,返回值就是Sub
- 重點: 5152 行和 5154 行中。Sub.options 合併了 2 個對象,分別是
Super.options(應該是父組件的一些參數了)
。第二個就是合併了自己的參數,其中 data 就在 5154 行中。後面的步驟還會說這個options
有點長,分開 2 張圖
3. 想辦法進入 init 方法看看
因爲在步驟 2 中我們留了個斷點,而一開始創建組件的方式是全局創建的。可能很多步驟沒有看到,把代碼改一改,改成局部組件,在 debugger 一下
代碼改成這樣子,因爲之前留有斷點,所以就無須 debugger 了,刷新即可直接到我們定好的斷點裏面去:
var ComponentA = {
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
data() {
return { count: 0 }
}
}
var app = new Vue({
el: '#app',
components: {
'component-a': ComponentA
}
})
能回到 Sub
裏面。說明我們之前摸索的步驟被調用了。Sub 方法被調用,纔會執行到init
。那我們在 init。返回上一步,看下是誰調用的。
看來這一步就是開始 new 一個新的組件。所以觸發到了 init 方法
這次進來總算看到有下一步的函數
4. 進入組件的 init 方法中
- 特別熟悉的感覺。沒錯!就是
new Vue
那個過程!畢竟組件也有自己的生命週期,參數,子組件,所以又回到了這裏 - 那
initState
-initData
的過程我就不重複。不清楚的可以再看上面new Vue
的過程。
5. 組件的 data 在 initData
中的作用
這裏開始繞了。思路要清晰
-
回想步驟 2 extend 函數中 5152 行。和 5154 行。是不是存儲了組件的
options
。 -
那在下圖的
4700
行中。vm 就是當前的組件。他的options
就是來自組件註冊時,生成的Sub
對象
6. 這時候抽象出來一些代碼
- Sub.options 是組件註冊的時候就開始有值了。所以我們也給個默認值演示
- Sub.prototype.init 估計是後期賦值,賦值爲創建 vue 的生命週期的函數。所以我們也給他來一個簡化版的函數,只模擬賦值 this.data 的過程,看一下效果
var Sub = function() {
this.init()
}
Sub.prototype = {}
Sub.prototype.constructor = Sub
Sub.prototype.init = function() {
this.data = typeof Sub.options.data === 'function' ? Sub.options.data() : Sub.options.data
}
Sub.options = {} // 等下會給默認值
7. 根據抽象出來的代碼,模擬 new 幾個組件
第一次嘗試用的是 data 對象形式:
::: tip 原理和最上面的案例 1
一樣
因爲 data 是引用類型。並且一開始 Sub.options 就是有值的,在創建新組件的時候拿的都是同一個地方的值
:::
// 上面也說了。先給sub.options來個默認值。模擬傳入的參數
Sub.options = {
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
data: {
count: 0
}
}
// 結合步驟6的代碼。創建3個組件
var component1 = new Sub()
var component2 = new Sub()
var component3 = new Sub()
console.log(component1) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
// 看着好像沒啥問題?我們來修改一個組件的值
component1.data.count = 1
// 傳說中的組件中值會相互影響情況出現了
console.log(component1) // {data:{count:1}}
console.log(component2) // {data:{count:1}}
console.log(component3) // {data:{count:1}}
如果改成函數的形式呢?
::: tip 原理和案例 2
一樣。
雖然這時候 Sub.options 拿到也是同一個地方的值。可是 Sub.options.data 已經是函數類型,而不是引用類型。函數執行後,返回的值都是不用堆內存的地址,所以修改某一個Sub實例(組件的值)
其餘的組件都不會受到影響
:::
// 上面也說了。先給sub.options來個默認值。模擬傳入的參數
Sub.options = {
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
data() {
return {
count: 0
}
}
}
// 結合步驟6的代碼。創建3個組件
var component1 = new Sub()
var component2 = new Sub()
var component3 = new Sub()
console.log(component1) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
console.log(component3) // {data:{count:0}}
component1.data.count = 2
// 現在就不會互相影響了
console.log(component1) // {data:{count:2}}
console.log(component2) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
8. 最後總結一下 demo
可以自己試着改一改。跑一跑
var Sub = function() {
this.init()
}
Sub.prototype = {}
Sub.prototype.constructor = Sub
Sub.prototype.init = function() {
this.data = typeof Sub.options.data === 'function' ? Sub.options.data() : Sub.options.data
}
Sub.options = {
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
data() {
return {
count: 0
}
}
}
// 結合步驟6的代碼。創建3個組件
var component1 = new Sub()
var component2 = new Sub()
var component3 = new Sub()
console.log(component1) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
console.log(component3) // {data:{count:0}}
component1.data.count = 2
// 現在就不會互相影響了
console.log(component1) // {data:{count:2}}
console.log(component2) // {data:{count:0}}
console.log(component2) // {data:{count:0}}
原文首發:Vue 組件 data 爲什麼必須是函數 這是新的博客地址,感興趣可以看看