關於Vue-extend和VueComponent問題小結

在一個非單文件組件中(一個文件中包含n個組件,最常見的就是單個html文件中存在多個組件),如果我們需要在這個文件中創建n個組件,然後再頁面上展示,這時候我們就需要先定義組件,然後註冊組件,最後使用組件。在定義組件這一步,我們就需要使用到 extend 這個方法。當然,也可以在一個html文件中使用多個 new Vue () 來註冊組件,但是這麼做有問題,下面再說。

Vue.extend(option)

官方文檔解釋:使用基礎 Vue 構造器,創建一個“子類”。參數是一個包含組件選項的對象。

組件選項:templete模板(el屬性隻能在 new Vue() 時使用)、data數據、methods方法、computed計算屬性等等正常組件擁有的。

我的理解:首先 extend 是 Vue 自帶的一個全局方法,該方法接收一個對象作為參數,在實例化的時候,通過傳遞的參數將一個空的 Vue實例 進行擴展,並以此來創造出一個 Vue 子類,也就是我們說的 Vue 組件。

使用方法:

1、非單文件組件內部所有組件全部使用 Vue.extend() 方式註冊(未指定根組件,無法渲染)

2、非單文件組件中使用 new Vue() 註冊根組件,其餘子組件則使用 Vue.extend() 方式註冊。

3、全部使用 new Vue() 註冊組件(若是存在組件嵌套,則子組件內部雙向綁定的的數據失效)

1、全部使用 Vue.extend() 方式註冊組件

const vm2 = Vue.extend({
  template:
    `<div id='root2'>
    {{msg}}
  </div>`,
  data() {
    return {
      msg: 'root2',
      count: 2,
      pets: 'dog'
    }
  }
})
 
const vm1 = Vue.extend({
  template:
    `<div id='root1'>
      {{msg}}
      <!-- 使用 root2 組件 -->
      <vm2 />
    </div>`,
  data() {
    return {
      msg: 'root1',
      count: 1,
      pets: 'dog'
    }
  },
  components: {
    // 註冊子組件
    vm2
  }
})

無法展示,頁面也不會報錯,因為根本沒有指定根組件進行渲染

2、使用第二種方式分別註冊子組件和根組件

註冊子組件

const vm1 = Vue.extend({
  template:
  `<div id='root2'>
    {{msg}}
    <input v-model="count" />
  </div>
  `,
  data() {
    return {
      msg: 'root2',
      count: 2,
      pets: 'dog'
    }
  }
})

註冊根組件

// 註冊根組件
const vm = new Vue({
  el: '#root1',
  data() {
    return {
      msg: 'root1',
      count: 1,
      pets: 'dog'
    }
  },
  components: {
    // 註冊子組件
    vm1
  }
})

根組件以及子組件使用

<div id='root1'>
  {{msg}}
  <input v-model="count" />
  <!-- 使用 root2 組件 -->
  <vm1 />
</div>

頁面展示效果正常,且雙向綁定數據正常

3、全部使用 new Vue 定義組件。其實在正常的開發中,這個方法用的極少,因為我們的開發一般都是單文件組件,在工程中每個組件都是通過 new Vue() 創建的,直接掛載到 根組件 app上。但是在單文件組件中,我們一般不使用多個 new Vue() 來創建組件,這是因為在使用 new Vue() 時,必須要傳入一個 el 屬性,這樣會導致html頁面上存在多個根節點,如果你的根節點嵌套瞭,那嵌套的根節點中綁定的數據會失效,不會展示。例如:

<div id='root1'>
  {{msg}}
  <input v-model="count" />
  <select name="pets" id="pet-select" v-model="pets">
    <option value="">--Please choose an option--</option>
    <option value="dog">Dog</option>
  </select>
 
  <div id='root2'>
    {{msg}}
    <input v-model="count" />
    <select name="pets" id="pet-select" v-model="pets">
      <option value="">--Please choose an option--</option>
      <option value="dog">Dog</option>
    </select>
  </div>
</div>

在這個 html 文件中,存在兩個根節點 ,root1、root2,兩個跟節點內部的子節點完全一樣,綁定的數據也完全一樣,但是 root1根節點包裹住瞭 root2根節點。

const vm = new Vue({
  el: '#root1',
  data() {
    return {
      msg: 'root1',
      count: 1,
      pets: 'dog'
    }
  },
})
 
const vm1 = new Vue({
  el: '#root2',
  data() {
    return {
      msg: 'root2',
      count: 2,
      pets: 'dog'
    }
  }
})

按照原本的想法,兩個節點展示的數據應該完全一樣,但是在頁面上的效果是這樣的。

可以看到,隻有外部的root1根節點展示瞭對的數據, root2的根節點數據要麼為空不展示,要麼展示的是錯誤數據。

如果我們使用 Vue.extend() 來註冊子組件又會是什麼情況呢?

首先,註冊root2組件,其實就是將root2的所有節點放在瞭 templete 屬性內部,用字符串模板包裹

const vm1 = Vue.extend({
  template:
    `<div id='root2'>
    {{msg}}
    <input v-model="count" />
    <select name="pets" id="pet-select" v-model="pets">
      <option value="">--Please choose an option--</option>
      <option value="dog">Dog</option>
    </select>
  </div>`,
  data() {
    return {
      msg: 'root2',
      count: 2,
      pets: 'dog'
    }
  }
})

2、在父組件中註冊 root2 組件

const vm = new Vue({
  el: '#root1',
  data() {
    return {
      msg: 'root1',
      count: 1,
      pets: 'dog'
    }
  },
  components: {
    vm1
  }
})

3、使用 root2 組件

<div id='root1'>
  {{msg}}
  <input v-model="count" />
  <select name="pets" id="pet-select" v-model="pets">
    <option value="">--Please choose an option--</option>
    <option value="dog">Dog</option>
  </select>
  <!-- 使用 root2 組件 -->
  <vm1 />
</div>

4、頁面效果

結論:如果是在非單組件文件(或者是html頁面),最好是隻用 一個new Vue()註冊一個根組件,其餘子組件則是用 Vue.extend() 註冊。否則如果使用 new Vue() 註冊所有組件的話,若是存在組件包裹的情況,則被包裹的組件內部雙向數據綁定會失效。

VueComponent

在組件定義之後,我們其實還沒有去理解這個過程和內部操作,下面我們就來剖析一下,看看在 Vue.extend() 之後,發生瞭什麼。

首先,我們來看看 Vue.extend() 之後,返回的是一個什麼東西。

樣例代碼就不貼瞭,就是上面的 vm1 實例。打印 vm1 之後,看看是個啥

打印之後發現,這玩意是個函數,而且還是個構造函數。在這個函數裡面啥操作也沒做,隻不過調用瞭 _init() 方法。

Vue.extend = function (extendOptions) {
 /*****其餘操作***/
  var Sub = function VueComponent(options) {
    console.log("VueComponent被調用瞭");
    this._init(options);
  };
  /*****其餘操作***/
  return Sub;
};
}

所以說,

1、組件的本質就是一個 【VueComponent 的構造函數】,且這個函數是 Vue.extend() 生成的。

2、在使用組件時,我們隻需要寫上組件標簽,Vue 會自動幫我們生成 組件的實例對象( 因為組件的本質就是一個構造函數,構造函數被調用之後,當然會產生實例對象),即 Vue 幫我們執行的 new VueCopmonennt(options)

3、特別註意,每次調用 Vue.extend(),返回的都是一個新的組件,因為是通過函數返回的。這個地方我們看看上面的源碼就能知道,因為 每次調用之後返回的 Sub 都是不一樣的。

4、關於this指向,

a、通過 Vue.extend() 配置的組件,在data,methods,component,watch等可能用到 this 的地方,this 指向的是 【VueComponent 的實例對象】

b、通過new Vue() 配置的組件,在data,methods,component,watch等可能用到 this 的地方,this 指向的是 【Vue 的實例對象】

驗證一下:

分別在上面的實例 vm,vm1上配置 show 方法,在方法內部打印當前this

點擊按鈕之後,查看 this 指向

展開的地方太大瞭,就不展開瞭,但是在控制臺上對比發現,除瞭 一個是 Vue {} 一個是 VueComponent {} 之外,內部的所有屬性全部一致,包括數據劫持,數據代理,底層方法等等。

組件管理

之前說的Vue.extend(option) 這個模塊時,說到瞭非單文件組件內部,最好是使用Vue.extend(option) 來定義子組件,然後使用 new Vue(option) 來註冊根組件,從而使得 根組件好方便管理子組件,那麼從那裡能看出來管理狀態呢?

看看上面的this 指向問題,展開之後,發現 一個 $children屬性,這是一個數組

在 new Vue() 配置的組件中,發現存在一個 VueComponent {} 實例對象,這個對象指向的就是 vm1實例對象

而在 Vue.extend() 配置的組件中,發現這是一個空數組,這就是因為, 根組件調用瞭 vm1子組件,而 vm1子組件,內部是沒有調用別的子組件的。

這就是 Vue 的組件管理模式

總結

如何使用 Vue.extend() 定義一個組件:

1、Vue.extend(option) 和 new Vue(option) 創建組件時所傳入的 option 配置項幾乎一樣.

區別在於:

(a)、el不能指定:所有的組件最終隻會由一個vm管理,由這個vm中的 el 指定掛載容器

(b)、data必須寫成函數:避免組件復用時,數據存在引用關系。

Vue.extend() 定義組件的本質:

本質上是 調用瞭 VueComponent () 這個構造函數,去返回瞭一個 【VueComponent 實例對象】,且每次在Vue.extend() 調用時,返回的組件實例對象都不一樣

非單文件組件中定義根組件和子組件

原則上,默認一個非單文件組件中 隻存在一個 new Vue() 定義的根組件,可以有無數個 Vue.extend() 定義的子組件,這是因為,如果所有組件都用 new Vue() 定義,那麼如果存在組件包裹的情況,子組件內部雙向綁定的數據不會生效。如果都用 Vue.extend() 定義組件,那麼則沒有指定根組件,無法渲染。

this指向問題

使用 new Vue() 定義的組件,在組件內部能用到 this 的地方,this指向為【Vue實例對象】

使用 Vue.extend() 定義的組件,~~~~~~~~~~~~~this指向為【VueComponent實例對象】

官網補充

這裡說明瞭,使用 VueComponent 和 new Vue 的異同。但是其實還有一點:在 new Vue 中,傳遞的 data 屬性,可以是對象,也可以是函數( 當然,我們還是推薦函數寫法 ),但是在VueComponent 中傳遞的 data 屬性,則隻能是函數,因為 new Vue 註冊的是 根組件,不存在復用情況,data中的屬性不存在引用關系,不會導致數據錯亂,但是VueComponent 則不同

推薦閱讀: