Vue 關鍵概念介紹

2023-02-27 12:01:51

Vue現在已經迭代到 3+ 版本,閱讀官方檔案的過程中發現作者的一些理念和思路很合我口味,很多概念與方案都是基於解決實際問題提出並實現的,且在權衡利弊後勇於打破常規,比如如何看待關注點分離?。可見,Vue 之所以流行,不單單因為作者是國人,更應該是由於 Vue 作為新一代的解決方案提升了前端程式設計的體驗與效率。

本文介紹幾個核心概念。

選項式 vs 組合式

Vue 提供兩種程式碼的書寫風格——選項式組合式。可簡單理解為:前者物件導向程式設計;後者函數語言程式設計。

選項式:如果你有微信小程式的開發經驗,就知道選項式是什麼樣子,其實就是將元件的邏輯封裝到一個物件中,這個物件預定義多個欄位和方法(如 data、methods 和 mounted),開發人員需要在適當的地方組織程式碼。對於有物件導向語言背景的使用者來說,這通常與基於類的心智模型更為一致,同時,響應性相關的細節由框架本身處理,對初學者而言更為友好。

組合式:傳統的自由無約束的編碼風格,頂層就是各個成員變數和 functions,及一些勾點函數。似乎回到了 js 最初的模樣,在物件、類、prototype 這些概念普及以前,大多數程式碼就是一坨變數加一坨 function,然後 onclick 呼叫。但是 Vue 的組合式風格依託其底層的依賴注入系統,及完善的響應式 API,使得情況不像看上去那麼簡單,而是呈現出一種螺旋向上的味道,耐人尋味。

官方檔案對這兩種風格有一些比較,個人比較傾向於組合式,所以本文 Vue 程式碼都是組合式的。

響應式基礎

所謂響應式,就是檢視會隨著 JS 物件狀態的改變而自動改變(也就是MVVM模式),有這種效果的物件就叫作響應式物件(其實就是 JavaScript Proxy)。在組合式 API 中,我們需要顯式宣告響應式物件,有兩種方式——reactive()ref()

reactive()

該 API 返回的物件,是傳入物件的代理物件,其所有屬性及深層的子屬性,都是響應式的。響應式物件的內嵌物件也是響應式物件,就算給它賦值普通物件,如:

const proxy = reactive({})

const raw = {}
proxy.nested = raw  // proxy.nested 自動就是響應式物件

console.log(proxy.nested === raw) // false,代理物件和原始物件不是全等的

reactive() 的注意事項和原理

reactive() 有一定的侷限:它僅對物件型別有效(物件、陣列和 Map、Set 這樣的集合型別),而對 string、number 和 boolean 這樣的基礎型別無效;需要儘量避免對一個響應式變數重新賦值,除非我們有辦法將新物件和檢視重新建立連線;且當我們將響應式物件的屬性(基礎型別)賦值或解構至本地變數時,或是將該屬性傳入一個函數時,我們會失去響應性,如:

let state = reactive({ count: 0 })
// 官方檔案這裡的表述不是很準確,下面是我的表述:
// 表面上看是重新賦值的 state 狀態變化沒有引起檢視的變化,似乎響應連線丟失了,
// 其實原物件上的響應式連線還在,但是原物件在此處已無法繼續存取,所以響應式連線在不在不重要了,
// 重建響應就需要建立檢視和新物件的連線。
state = reactive({ count: 1 })  

let n = state.count  // 基礎型別賦值,失去響應性連線
n++  // 不影響 state

let { count } = state
count++  // 同上

// state.count 值傳遞給基礎型別形參,也失去響應性連線了
callSomeFunction(state.count)

對於這些情況,有後端經驗的同學如果將 reactive() 得到的響應式物件類比成參照型別物件就很好理解,這就是參照型別和值型別在使用過程中需要注意的一些點——變數賦值,如果是參照型別的話,那麼指向的是物件的記憶體地址(新舊物件的記憶體地址自然是不一樣的);如果是值型別,雖然程式碼看上去都是指向 state.count,其實是拷貝源值到自己的記憶體塊,拷貝完了之後就和源沒有關係了。

對於參照型別的「問題」,只要注意點就好了,但是在響應式的場景下,值型別的「拷貝」特性確實讓人有點鬧心。有沒有類似於後端的裝箱操作呢?

ref()

該 API 返回的也是響應式物件,它用於將值型別(基礎型別)封裝成參照型別(物件型別)。也就是說,我們將上一小節程式碼改造一下,就能保持基礎型別資料在各個變數間傳遞後的響應性,如下:

const state = reactive({
  count: ref(0)
})

let n = state.count // 現在 state.count 是參照型別,所以它和 n 指向的是同一個物件
n.value++  // 需要用 value 操作值
// 注意 value 也是響應式的,也就是傳遞給它的普通物件會自動轉為響應式物件,和 reactive() 那邊的情況一樣
// 同時要注意直接替換掉整個物件會導致出現響應連線丟失的問題(上面提到過)
n.value = { name: 'Tony' }

簡言之,我們可以將 ref 物件就看作參照型別物件,就能很快理解它的特性了。唯一要注意的是存取和操作它的值需要 .value,但是在某些時候框架也會幫我們自動解包(不需要使用 .value),可以參看官方檔案。

組合式函數

是利用 Vue 的組合式 API 來封裝和複用有狀態邏輯的函數。說白了,就是業務邏輯封裝,它表現形式不是物件,但是有狀態,狀態作為響應式物件對外暴露使用(如果有的話)。推測是為了和物件形式區分,才稱之為組合式函數(就像選項式風格和組合式風格的區別)。

Vue 2 的使用者可能會對mixins選項比較熟悉。它也讓我們能夠把元件邏輯提取到可複用的單元裡,然而 mixins 沒有自我範圍的約束,就像頁面裡使用<script>引入的 js 檔案,容易和其它 js 檔案產生命名衝突,物件來源也不清晰,編碼時不注意的話也容易產生模組和模組之間隱性的依賴。

其它幾個 API

nextTick():DOM 更新是有間隔時間的,在間隔時間內每個元件發生的所有狀態改變彙總後一次更新。可以給該函數傳遞一個回撥,在最近的一次 DOM 更新後執行。類似於 HTML5 新增的 window.requestAnimationFrame()

watchEffect(callback):callback 中涉及到的響應式物件狀態的變更會觸發 callback 執行,如下:

const count = ref(0)
watchEffect(() => console.log(count.value))  // 馬上執行一次,-> 輸出 0
count.value++  // -> 輸出 1

watch():同 watchEffect() 不同在於,watch() 需要顯式地給它傳遞要監聽的響應式物件。

構建工具 Vite

伴隨 Vue 3 一起出來的還有新的構建工具Vite。下面會簡單介紹 Vite 涉及到的關鍵技術和工具,以及同其它構建工具的比較。

ESM

不同於之前的CJS,AMD,CMD等,ESM是 ECMA 標準化模組系統,也就是說我們可以直接在瀏覽器中去執行 import,動態引入模組。作為 ECMA 標準,目前 ESM 已經得到 92% 以上瀏覽器的支援。

ESM 的執行可以分為三個步驟:

  1. 構建: 確定模板依賴關係,下載並將所有的檔案解析為模組記錄;
  2. 範例化: 將模組記錄轉換為一個模組範例,為所有的模組分配記憶體空間,依照匯出、匯入語句把模組指向對應的記憶體地址;
  3. 執行:執行程式碼,填充記憶體空間。

ESM 使用參照模式指向模組,也就是說如果參照的模組已經存在,那麼直接返回模組的記憶體地址。而 CJS 採用的是拷貝模式,即所有匯出模組都是獨立的範例。可見前者比後者的效率要高。

基於 ESM,還能做到按需載入模組(碰到 import 再去請求載入檔案)。但是我們一般只在開發環境下使用這個特性(不需要每次改動都導致整個 bundle 模組全量打包編譯),原因如下段所述。

儘管原生ESM現在得到了廣泛支援,但由於巢狀匯入會導致額外的網路往返,在生產環境中釋出未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。為了在生產環境中獲得最佳的載入效能,最好還是將程式碼預先進行Tree Shaking(移除那些沒被使用的程式碼)、懶載入和 chunk 分割(以獲得更好的快取)。

Rollup

Rollup就是基於 ESM 模組的打包工具,比WebpackBrowserify使用的 CommonJS 模組機制更高效。Rollup 能針對原始碼進行 Tree Shaking,以及 Scope Hoisting 以減小輸出檔案大小提升執行效能。

Esbuild

Esbuild提供了與WebpackRollup等工具相似的資源打包能力,但其打包速度卻是其他工具的 10~100 倍,原因有二:

  • 大多數前端打包工具都是基於 JavaScript 實現的,邊執行邊解釋。而 Esbuild 則選擇使用 Go 語言編寫,編譯為機器語言,在啟動的時候直接執行,效能更高;
  • JavaScript 本質上是一門單執行緒語言,直到引入Web Worker後才有可能在瀏覽器、Node 中實現多執行緒操作,目前大部分打包工具未必有使用 Web Worker 提供的多執行緒能力。而 GO 則沒這方面的「缺陷」,更不用說還有成熟的協程特性。

但是,雖然Esbuild快得驚人,並且已經是一個在構建庫方面比較出色的工具,但一些重要功能仍然還在持續開發中——特別是程式碼分割和 CSS 處理方面(ESM 小節提到的載入效能)。就目前來說,Rollup 在應用打包方面更加成熟和靈活。所以,我們一般在開發時,使用Esbuild進行構建,而在生產環境,則是使用 Rollup 進行打包。

HMR 熱更新

Webpack ——重新編譯,請求變更後模組的程式碼,使用者端重新載入。

Vite ——請求變更的模組,再重新載入。

Vite 通過chokidar監聽檔案系統的變更,使相關模組與其臨近的 HMR 邊界連線失效,只對發生變更的模組重新載入,這樣 HMR 更新速度就不會因為應用體積的增加而變慢而 Webpack 還要經歷一次打包構建。所以 HMR 場景下,Vite 表現也要好於 Webpack。


關於構建,需要注意的是,如果使用傳統 <script src="xxx.js"> 方式引入 Vue 的話,那麼就不會涉及到構建步驟,但同時將無法使用單檔案元件 (SFC) 語法。傳統方式一般用在對現有專案進行區域性 Vue 改造的場景下。