Vue 組件 data 爲什麼必須是函數(分析源碼,找到答案)

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,否則還是個默認值。
  • 接下來的步驟就是開始做一些代理,數據挾持的監聽proxyobserver 之類的 不在我們 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 爲什麼必須是函數 這是新的博客地址,感興趣可以看看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章