Vue 關鍵概念介紹
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 文件產生命名沖突,對象來源也不清晰,編碼時不注意的話也容易產生模塊和模塊之間隱性的依賴。
其它
組件
Props:自定義屬性;沒有被聲明為 Props 或 Emits 的屬性作為 Attributes 透傳給組件及其子組件(可設置不繼承父組件 Attributes)
Slots:向子組件傳遞模板片段的機制
provide 和 inject:父組件 provide,后代組件 inject;解決多層級組件中,深層子組件需要綁定屬性時 Prop 必需逐級透傳的問題
幾個函數
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。當然我們還可以繼續使用vue-cli,但是推薦使用 Vite。簡單地說,vue-cli 基于Webpack,每次代碼變動會引起整個項目的構建打包,導致開發階段效率較低;而 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 熱更新
Vite 還通過chokidar監聽文件系統的變更,使相關模塊與其臨近的 HMR 邊界連接失效,只對發生變更的模塊重新加載,這樣 HMR 更新速度就不會因為應用體積的增加而變慢。
關于構建,需要注意的是,如果使用傳統 <script src="xxx.js"> 方式引入 Vue 的話,那么就不會涉及到構建步驟,但同時將無法使用單文件組件 (SFC) 語法。傳統方式一般用在對現有項目進行局部 Vue 改造的場景下。

浙公網安備 33010602011771號