Skip to content

宣告式渲染與響應式心智

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

上一章我們已經用 Vite 建立好第一個 Vue 專案。接下來,先把畫面與資料之間的關係看清楚。先別急著找 DOM:用 Vue 時,我們更常做的是修改資料,剩下的畫面更新交給 Vue。

從資料驅動畫面開始

在傳統 DOM 操作中,我們常會先找到某個元素,再把文字塞進去。Vue 的寫法反過來:我們在 <script setup> 準備資料,在 <template> 描述畫面要如何呈現資料。

請打開第一章建好的 create-vue 專案,先把 src/App.vue 改成下面這個最小範例:

vue
<script setup>
const message = 'Hello Vue!'
</script>

<template>
  <main class="app">
    <h1>{{ message }}</h1>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

{{ message }} 稱為文字插值。Vue 會讀取 message 的值,並把它顯示在畫面上。這裡還沒有任何互動,不過已經是接下來會反覆看到的分工:資料準備在 <script setup>,畫面則寫在 <template>

讓資料變成響應式

如果資料會改變,就要讓 Vue 追蹤它。Composition API 裡最常用的起點是 ref()

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <main class="app">
    <p>目前數字:{{ count }}</p>
    <button type="button" @click="increment">
      加一
    </button>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>
試一試

目前數字:0

ref(0) 會建立一個可被 Vue 追蹤的響應式資料。按下按鈕時,increment() 修改 count.value,Vue 會自動把畫面上的 {{ count }} 更新成新的值。

常見錯誤:忘記 .value

<script setup> 裡,ref 的值要透過 .value 存取:

js
const count = ref(0)

count++          // 錯誤:count 是 const,重新指派會拋出 TypeError
count.value++    // 正確:這是在改 ref 裡面的值

要注意的是,count++ 並不是「改到 ref 裡面的值」,而是試圖把 count 這個變數重新指派成別的東西。因為我們用 const 宣告,JavaScript 會直接拋出錯誤,這反而是好事:錯誤訊息會提醒我們忘了 .value

這也回答了為什麼 ref() 通常搭配 const,而不是 letconst 限制的是 count 這個變數不能改指向別的東西,並不會讓 ref 裡的值變成唯讀,因此 count.value++ 完全合法。改用 let 在語法上也能運作,卻會放行下面這類重新指派:

js
let count = ref(0)

count = ref(10)   // 合法,但已經換掉原本的 ref
count = 10        // 也合法,count 從此不再是 ref

響應式狀態通常只需要修改 .value,不需要換掉 ref 本身,所以用 const 能把意外提早擋下來。除非真的需要重新指派變數,否則預設使用 const 會更安全,也更容易看出資料的身分沒有改變。

但在 <template> 裡,Vue 會自動解包 ref,所以畫面上寫 {{ count }} 即可,不需要寫成 {{ count.value }}

小提醒:還有一個 reactive()

Vue 也提供 reactive() 建立響應式物件。本章先統一使用較容易追蹤資料來源的 ref();兩者的差異與選擇方式,第三章說明響應式系統時會完整比較。

模板自動解包

模板自動解包是初學者最容易混淆的地方。規則只有兩條:

  • <script setup> 裡操作 ref:要寫 .value
  • <template> 裡顯示或綁定 ref:直接寫變數名稱。

不過自動解包有個前提:它只適用於模板可以直接看到的頂層變數,也就是像本章這樣直接在 <script setup> 頂層宣告的 ref。如果 ref 被包在一般物件的屬性裡,模板運算式就不會自動解包它。本章的範例都是頂層宣告,可以放心套用這個規則;巢狀結構的細節,我們留到第三章討論響應式系統時再展開。

下面這個範例同時使用 ref 與模板插值:

vue
<script setup>
import { ref } from 'vue'

const name = ref('Kuro')

function rename() {
  name.value = 'Vue Reader'
}
</script>

<template>
  <main class="app">
    <p>Hi, {{ name }}!</p>
    <button type="button" @click="rename">
      換名字
    </button>
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

按下按鈕後,我們沒有直接操作 DOM,也沒有自己去找 <p> 標籤。只要資料改變,畫面就會跟著資料重新呈現。

Vue 的更新流程

可以先用這張圖建立直覺:

Vue 的更新流程:從使用者操作或非同步資料開始,依序修改響應式狀態、排程元件更新、重新計算模板、更新必要的 DOM,最後由瀏覽器顯示結果

這張圖先記住一件事:我們修改的是狀態,Vue 負責安排畫面更新。資料可能來自按鈕,也可能來自 API 或計時器;Vue 會批次處理這些變化,最後只更新必要的 DOM。

用 MVVM 理解這段流程

如果你接觸過 MVVM (Model-View-ViewModel),也可以借用它來理解前面的流程。三個角色大致如下:

角色負責什麼在 Vue 應用裡常見的對應
Model應用程式的資料與領域規則API 回傳資料、一般 JavaScript 物件、Store
View使用者看到與操作的畫面<template> 描述的介面,以及最後產生的 DOM
ViewModel準備畫面需要的狀態、接收操作,並銜接 Model 與 View元件中的響應式狀態與操作函式,加上 Vue 的響應式與渲染機制

以前面的計數器為例,使用者在 View 按下按鈕後,元件的 increment() 會修改 count.value;Vue 追蹤到這個變化,再把新的數字更新到 View。這段元件邏輯就扮演了類似 ViewModel 的橋接角色。計數器很小,沒有另外抽出領域資料層;在真正的應用裡,Model 可能來自 API 或 Store,再由元件整理成畫面需要的狀態。

那麼,Vue.js 現在還算不算 MVVM 框架?

比較精確的說法是:Vue 的設計可以用 MVVM 幫助理解,但 Vue 並不要求應用程式嚴格遵循 MVVM 架構。

早期 Vue 文件常用 ViewModel 解釋 Vue instance;現代 Vue 則把自己定位為漸進式 JavaScript 框架,強調的是宣告式渲染、響應式系統與元件化。使用 Composition API 時,我們也不需要建立一個名為 ViewModel 的類別,refcomputed 與函式可以依功能自由組合。

所以,MVVM 在這裡是一張理解資料與畫面如何連動的地圖,不是每個 Vue 專案都必須照著套用的目錄規格。真正需要記住的仍是:畫面負責描述結果,程式修改狀態,Vue 負責把兩者同步起來。

接下來要介紹的模板、事件與 computed,都是建立在這個資料驅動畫面的觀念上。