# 1-2 Vue.js 的核心: 實體

接續前一篇的內容,這裡我們開始說明 Vue.js 的實體物件與主要的 API。

當我們透過 <script> 標籤將 Vue.js 引入,此時瀏覽器會新增一個 Vue 的全域變數, 這個全域變數提供了幾個主要的功能,一是作為 Vue 實體物件的建構子函數 (Constructor function) , 另一方面也作為全域 API 的宿主物件,提供開發者透過 Vue 物件來設定 Global API,如 Vue.configVue.directiveVue.nextTick ...等。

# Vue.js 的實體物件

如同前一篇所說,不管是 2.x 的 new Vue({...}) ,或是 3.0 的 createApp({...}), 一開始我們都需要先建立一個 Vue 的物件實體,並且將這個物件指定至某個變數,如範例裡的 vm 之中。

// Vue 2.x
const vm = new Vue({
  data: {
    message: 'Hello Vue!'
  }
});

// 透過 $mount 掛載至指定的網頁節點
vm.$mount('#app');
1
2
3
4
5
6
7
8
9
// Vue 3.0
const vm = Vue.createApp({
  data () {
    return {
      message: 'Hello Vue 3.0!'
    }
  }
});

vm.mount('#app');
1
2
3
4
5
6
7
8
9
10

像這樣,透過 new Vue() (2.x) 或是 Vue.createApp() (3.0) 我們會得到一個新的物件,並將它指定給 vm 變數,而新生成的這個物件,我們就將它稱為「Vue.js 的實體物件」。 Vue.js 就是透過這個實體物件來掛載在網頁上的某個 DOM 節點,我們就可以來控制網頁節點對應的內容。

另外,讀者可能已經注意到,當我們建立 Vue.js 的實體物件時,還會引入一個物件參數,這個物件就是 Vue.js 實體的核心,我們通常稱它叫 options 物件。 在這個物件中,我們會定義各式各樣 UI 與模板相關的狀態、事件,以及要被呼叫的函式 (或稱方法, methods) 等,這也是本章節說明的主要內容。

小提醒

當我們建立 Vue.js 的實體物件時,可以將它指定到某個變數,如:

const vm = Vue.createApp({
  // options
});

// 等待需要的時候再 mount 到某個節點上
vm.mount('#app');
1
2
3
4
5
6

也可以不指定變數,就直接將 Vue.createApp 生成的物件實體拿來使用:

// 建立 Vue 的物件實體即掛載
Vue.createApp({
  // options
}).mount('#app');
1
2
3
4

實際開發中,我們可能會同時在一個網頁裡建立多個 Vue.js 實體,當這些實體需要進行狀態傳遞或是方法的調用時,就會透過這些變數來進行溝通。 當然我們也可以透過這個變數來監測,甚至是操作這個實體內部狀態的改變。

# 掛載至 DOM

建立 Vue.js 實體後的下一步,就是要將這個實體掛載至網頁的 DOM 節點以取得網頁的控制權了 (怎麼感覺有點像寄生)。 將 Vue.js 實體掛載至 DOM 有幾個方式,在 2.x 最常見的是透過 options API 的 el 屬性:

<div id="app"></div>
1
// for Vue 2.x
const vm = new Vue({
  el: '#app'
});
1
2
3
4

在 Vue.js 2.x 除了透過 Options 物件內的 el 屬性指定目標 DOM 節點外, 我們還能透過 Vue 實體提供的 $mount 方法來指定:

// Vue 2.x
const vm = new Vue({
  // options
});

// 新增節點然後加入至 body
const el = document.createElement('div');
document.body.appendChild(el);

// 將 Vue 實體掛載至新生成的節點
vm.$mount(el);
1
2
3
4
5
6
7
8
9
10
11

像這樣,我們可以先將 Vue 實體物件 new 出來而不透過 el 屬性掛載,而是在適當的時機透過 $mount 方法才指定掛載。 假如我們要掛載的目標節點是透過動態的方式生成,等待節點生成後再透過 $mount 方法指定掛載,就可以排除 Vue.js 一開始找不到節點的問題。

到了 Vue.js 3.0,這個 el 屬性則是被 mount() (注意沒有 $ 符號) 所取代:

// 注意:Vue.js 3.0 無法使用 el 屬性
const vm = Vue.createApp({
  // options
});

vm.mount('#app');
1
2
3
4
5
6

像這樣,我們透過 el (2.x) 或 mount() (3.0) 指定了掛載的 DOM 節點後,就可以順利將這個 Vue.js 實體掛載至 <div id="app"> 這個節點。

小提醒

el 屬性與 mount() 的目標節點可以是 CSS 選擇器,也可以是 DOM 物件 (如 document.querySelector 取得的 DOM 物件)。 在 CSS 選擇器的狀況下,並沒有硬性規定只能用 id 作為選擇器的條件,也可以用 class 甚至是 tagName 作為選擇條件。

但需要注意的是,若同時有多個符合條件的元素,只有被選出的「第一個」元素會被 Vue 實體掛載。

# 定義狀態與網頁模板的映射關係

在 Vue 實體掛載完成之後,這個時候....... 應該什麼都不會發生。

為什麼呢? 還記得嗎,前面說過,Vue.js 採用的是宣告式渲染與 MVVM 模式來操作網頁內容, 所以我們需要先將 UI 層 (也就是網頁畫面) 會用到的資料/狀態,預先定義在 Vue.js 的實體當中,再由 HTML 模板進行輸出。

在 Vue 實體的定義的狀態,就是透過 data 屬性來儲存。

const vm = Vue.createApp({
  // 實體所回傳的狀態會以物件 key-value 的形式
  // 且在 Vue 3.0 開始,data 將強制以 function 的形式出現
  data () {    
    return {
      name: '008JS'
    }
  }
});

vm.mount('#app');
1
2
3
4
5
6
7
8
9
10
11

像這樣,我們在 Vue.js 的實體中定義了 name 這個狀態,而它的值為 '008JS' 的字串。

那麼我們要怎麼將這個狀態輸出至網頁呢? 就必須要依靠 Vue.js 的模板語法。 Vue.js 的模板語法採用的是 Mustache 語法,也就是在 HTML 以兩個大括號 {{ }} 來表示,大括號的內容是 Vue.js 實體物件內的狀態。

以剛剛的範例來說,我們可以在指定的 <div id="app"> 加上:

<div id="app">
  {{ name }} 好棒棒!
</div>
1
2
3

此時網頁會將模板內的內容進行解析,然後輸出 008JS 好棒棒! 的文字。

狀態與網頁模板的映射

在 Vue.js 的 Mustache 模板語法裡,除了單純將 data 的內容渲染至畫面外,也可以在裡面做一些簡單的運算:

<div id="app">
  單價: {{ price }}, 
  數量: {{ quantity }}, 
  總金額共 {{ price * quantity }} 元
</div>

<!-- 
  單價: 100, 數量: 10, 總金額共 1000 元
-->
1
2
3
4
5
6
7
8
9
const vm = Vue.createApp({
  data () {
    return {
      price: 100,
      quantity: 10
    }
  }
});

vm.mount('#app');
1
2
3
4
5
6
7
8
9
10

像這樣,模板語法允許開發者在裡面使用運算式,並且會將運算後的「值」輸出到畫面上。

小提醒

若你所使用的後端模板引擎剛好也採用 Mustache 兩個大括號 {{ }} 的模板語法來表示, 此時在執行上可能會造成衝突,那麼可以利用 Vue.js 提供的 delimiters 屬性 來重新定義模板語法,如:

<div id="app">
  <!-- 取代原本的 {{ message }} -->
  %{ message }%
</div>
1
2
3
4
// vue 3.x
const app = Vue.createApp({
  delimiters: ['%{', '}%'],
  data () {
    return {
      message: 'Hello World!'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
8
9

這樣也可以達到一樣的效果,但需要注意的是,這個屬性只支援在「瀏覽器」即時編譯的版本中使用。 若用 Vue-loader (如 Vue-CLI / vite 等) 編譯的狀況下,並未支援 delimiters 這個屬性。

基本上,所有可以作為合法 JavaScript 變數的內容都可以成為 data 屬性的內容。 Vue.js 會自動將 data 內的屬性加上 gettersetter 的特性,以便監控狀態的更新 (響應式更新)。

而 Vue 實體被建立之後,Vue.js 就會自動為這個實體加上 $data 屬性,我們就可以透過 vm.$data.XXX 來操作內部狀態:

const vm = Vue.createApp({
  data () {
    return {
      name: '008JS'
    }
  }
}).mount('#app');

vm.$data.name = '30cm';
1
2
3
4
5
6
7
8
9

此時在網頁上的模板,更新後就會變成 30cm 好棒棒! 的字樣了。

另外,這裡需要注意的是,若我們在 createApp 時並未將此實體同時掛載至某個 DOM 節點,在變動 vm.$data.name 會跳出無法設定的錯誤:

const vm = Vue.createApp({
  data () {
    return {
      name: '008JS'
    }
  }
});

const vMountedInstance = vm.mount('#app');

// TypeError: Cannot set property 'name' of undefined
vm.$data.name = '30cm';

// It's ok
vMountedInstance.$data.name = '30cm';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

直到執行至 vm.mount('#app') 之後才會產生對應的 $data。 關於此處細節,在本書的【元件的生命週期與更新機制】一節中會有更詳細的說明。

# 小心 data 共用帶來的污染

需要特別注意的是,若是在同個網頁有超過一份 Vue 的實體,這時又想為了讓這些 Vue 實體物件都有著同樣的 data 屬性格式,我們可能會將原本的 data 在實體外面定義:

const dataObj = {
  name: '008JS'
};

const vm1 = Vue.createApp({
  data () {
    return dataObj
  },
}).mount('#app');

const vm2 = Vue.createApp({
  data () {
    return dataObj
  },
}).mount('#app2');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- vm1 -->
<div id="app">
  <input type="text" v-model="name">
</div>

<!-- vm2 -->
<div id="app2">
  <input type="text" v-model="name">
</div>
1
2
3
4
5
6
7
8
9
試一試

在網頁剛載入時,執行結果看似沒有問題,但如果我們嘗試著去改變 vm1.$data.namevm2.$data.name 的值,就會發現:

vm1.$data.name = '阿不就';
1

這時候,我們的 app1app2 會因為共用一份 data ,結果就都出現 阿不就好棒棒 的結果。

像這樣的情況就需要多加小心注意。

最簡單的解決方式,就是透過 ES6 的物件解構 (淺拷貝) 或 JSON.parse(JSON.stringify(...)) (深拷貝) 將兩者變成不同的物件回傳即可避開這個問題。

const vm1 = Vue.createApp({
  data () {
    return { ...dataObj }
  },
}).mount('#app');

const vm2 = Vue.createApp({
  data () {
    return { ...dataObj }
  },
}).mount('#app2');
1
2
3
4
5
6
7
8
9
10
11

注意

在 Vue.js 的實體當中,以底線 _ 或錢字號 $ 作為開頭的屬性,不會被加上 gettersetter 的特性,如:

<div id="app">
  {{ $hello }}
</div>
1
2
3
const vm = Vue.createApp({
  data () {
    return {
      $hello: 'Hello Vue.js!'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7

此時畫面不會出現任何結果,而且還會在 console 主控台看到這樣的錯誤訊息: [Vue warn]: Property "$hello" must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.

這是由於以底線 _ 或錢字號 $ 作為開頭的屬性,可能會與 Vue.js 內建的屬性與 API 名稱產生衝突,所以應該盡量避免。

# template 模板屬性

除了直接使用 HTML 當作 Vue.js 模板外,我們也可透過 Options 裡的 template 屬性來定義:

<div id="app"></div>
1

 







const vm = Vue.createApp({
  template: `<div>{{ greeting }} 好棒棒!</div>`,
  data () {
    return {
      greeting: 'Hello Vue.js!'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
8

我們在 Vue 實體的 Options 加入 template 屬性 ,這個時候 Vue.js 會將 template 內的 HTML 當作模板來使用,顯示的效果與直接使用 HTML 模板是一樣的。

小提醒: Vue 2.x 的模板注意!

需要注意的是,無論是使用 HTML 作為模板,或透過 template 屬性來指定內容,
元件 (沒錯,每個 Vue 實體就是一個元件) 當中的第一層只能也必需要有一個元素

換言之,像:

// for Vue 2.x
const vm = new Vue({
  // 沒有根元素 (root element)
  template: `{{ name }} 好棒棒!`,
});
1
2
3
4
5

或是

// for Vue 2.x
const vm = new Vue({
  // 超過一個根元素 
  template: `<div>{{ name }} 好棒棒!</div> <div>我也好棒棒!</div>`,
});
1
2
3
4
5

都是不合法的, console 主控台會出現 [Vue warn]: Component template should contain exactly one root element 的錯誤訊息。

由於 Vue 3.0 採用 fragments 來進行模板 DOM 編譯,所以這個問題就不再存在了。

// in Vue 3.0, It's ok.
const vm = Vue.createApp({
  template: `{{ greeting }} 好棒棒!`,
  data () {
    return {
      greeting: 'Hello Vue.js!'
    }
  }
});

vm.mount('#app');
1
2
3
4
5
6
7
8
9
10
11

到目前為止,我們已經學會如何新增 Vue 實體,以及定義狀態與網頁模板的映射關係,讓網頁畫面可以輸出 Vue 實體中對應的資料了。 Vue 實體 Options API 所提供的屬性還有很多,在後續的篇章中我們還會常常見到它們。 接著在下一個小節中將繼續介紹,當資料狀態被更新時,如何偵測資料的更新與加工。

Last Updated: 12/27/2020, 9:31:17 PM