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 的執行可以分為三個步驟:
- 構建: 確定模板依賴關係,下載並將所有的檔案解析為模組記錄;
- 例項化: 將模組記錄轉換為一個模組例項,為所有的模組分配記憶體空間,依照匯出、匯入語句把模組指向對應的記憶體地址;
- 執行:執行程式碼,填充記憶體空間。
ESM 使用引用模式指向模組,也就是說如果引用的模組已經存在,那麼直接返回模組的記憶體地址。而 CJS 採用的是複製模式,即所有匯出模組都是獨立的例項。可見前者比後者的效率要高。
基於 ESM,還能做到按需載入模組(碰到 import 再去請求載入檔案)。但是我們一般只在開發環境下使用這個特性(不需要每次改動都導致整個 bundle 模組全量打包編譯),原因如下段所述。
儘管原生ESM
現在得到了廣泛支援,但由於巢狀匯入會導致額外的網路往返,在生產環境中釋出未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。為了在生產環境中獲得最佳的載入效能,最好還是將程式碼預先進行Tree Shaking
(移除那些沒被使用的程式碼)、懶載入和 chunk 分割(以獲得更好的快取)。
Rollup
Rollup
就是基於 ESM 模組的打包工具,比Webpack
和Browserify
使用的 CommonJS 模組機制更高效。Rollup 能針對原始碼進行 Tree Shaking,以及 Scope Hoisting 以減小輸出檔案大小提升執行效能。
Esbuild
Esbuild
提供了與Webpack
、Rollup
等工具相似的資源打包能力,但其打包速度卻是其他工具的 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 改造的場景下。