本文整理自拉勾網Vue.js 3.x 源碼課程,講師是來自Zoom的大牛黃軼,非常感謝! 本人僅補充一些參考資料。 1. Vue.js框架的演進過程 Vue.js 從 1.x 到 2.0 版本,最大的升級就是引入了虛擬 DOM 的概念。 Vue.js 2.x 的版本痛點問題: 源碼自身的維護性; 數 ...
本文整理自拉勾網Vue.js 3.x 源碼課程,講師是來自Zoom的大牛黃軼,非常感謝! 本人僅補充一些參考資料。
1. Vue.js框架的演進過程
Vue.js 從 1.x 到 2.0 版本,最大的升級就是引入了虛擬 DOM 的概念。
Vue.js 2.x 的版本痛點問題:
- 源碼自身的維護性;
- 數據量大後帶來的渲染和更新的性能問題;
- 雞肋 API;
- TypeScript 支持不佳;
- ...
Vue.js 3.x 帶來的優化
-
源碼優化
-
性能優化
-
語法Api優化
2. Vue.js 3.0 優化概覽
那麼接下來,我們就一起來看一下 Vue.js 3.0 具體做了哪些優化。
2.1 源碼優化
首先是源碼優化,也就是祖師爺對於 Vue.js 框架本身開發的優化,它的目的是讓代碼更易於開發和維護。
源碼的優化主要體現在使用 monorepo
和 TypeScript
管理和開發源碼,這樣做的目標是提升自身代碼可維護性。接下來我們就來看一下這兩個方面的具體變化。
2.1.1 更好的代碼管理方式:monorepo
-
什麼是monorepo?
就是把多個項目放在一個倉庫裡面,相對立的是傳統的 MultiRepo 模式,即每個項目對應一個單獨的倉庫來分散管理。
-
monorepo 解決什麼問題?
-
多個
repo
難以管理,編輯器需要打開多個項目; -
某個模塊升級,依賴改模塊的其他模塊需要手動升級,容易疏漏;
-
公用的npm包重覆安裝,占據大量硬碟容量,比如打包工具
webpack
會在每個項目中安裝一次; -
對新人友好,一句命令即可完成所有模塊的依賴安裝,且整個項目模塊不用到各個倉庫去找;
-
-
monorepo 在vue.js 3.x 中的應用
源碼的優化體現在代碼管理方式上。
Vue.js 2.x 的源碼托管在 src 目錄,然後依據功能拆分出了
- compiler(模板編譯的相關代碼)
- core(與平臺無關的通用運行時代碼)
- platforms(平臺專有代碼)
- server(服務端渲染的相關代碼)
- sfc(.vue 單文件解析相關代碼)
- shared(共用工具代碼)
等目錄:
而到了 Vue.js 3.0 ,整個源碼是通過 monorepo
的方式維護的,根據功能將不同的模塊拆分到 packages 目錄下麵不同的子目錄中:
可以看出相對於 Vue.js 2.x 的源碼組織方式,monorepo 把這些模塊拆分到不同的 package 中,每個 package 有各自的 API、類型定義和測試。
這樣做的優勢在於:
- 使得模塊拆分更細化,職責劃分更明確,模塊之間的依賴關係也更加明確
- 開發人員也更容易閱讀、理解和更改所有模塊源碼,提高代碼的可維護性。
- 一些 package(比如 reactivity 響應式庫)是可以獨立於 Vue.js 使用的,這樣用戶如果只想使用 Vue.js 3.0 的響應式能力,可以單獨依賴這個響應式庫而不用去依賴整個 Vue.js,減小了引用包的體積大小,而 Vue.js 2 .x 是做不到這一點的。
參考資料:
2.1.2 有類型的 JavaScript:TypeScript
源碼的優化還體現在 Vue.js 3.0 自身採用了 TypeScript 開發。
Vue.js 1.x 版本的源碼是沒有用類型語言的,祖師爺用 JavaScript 開發了整個框架,但對於複雜的框架項目開發,使用類型語言非常有利於代碼的維護,因為它可以在編碼期間幫你做類型檢查,避免一些因類型問題導致的錯誤;也可以利於它去定義介面的類型,利於 IDE 對變數類型的推導。
因此在重構 2.0 的時候,祖師爺選型了 Flow(Flow是JavaScript代碼的靜態類型檢查器。)。
參考資料:
但是在 Vue.js 3.0 的時候拋棄 Flow 轉而採用 TypeScript 重構了整個項目,這裡有兩方面原因:
首先,Flow 是 Facebook 出品的 JavaScript 靜態類型檢查工具,它可以以非常小的成本對已有的 JavaScript 代碼遷入,非常靈活,這也是 Vue.js 2.0 當初選型它時一方面的考量。但是 Flow 對於一些複雜場景類型的檢查,支持得並不好。
其次,Vue.js 3.0 拋棄 Flow 後,使用 TypeScript 重構了整個項目。 TypeScript提供了更好的類型檢查,能支持複雜的類型推導;由於源碼就使用 TypeScript 編寫,也省去了單獨維護 d.ts 文件的麻煩;就整個 TypeScript 的生態來看,TypeScript 團隊也是越做越好,TypeScript 本身保持著一定頻率的迭代和更新,支持的 feature 也越來越多。
2.2 性能優化
2.2.1 源碼體積優化
首先是源碼體積優化,我們在平時工作中也經常會嘗試優化靜態資源的體積,因為 JavaScript 包體積越小,意味著網路傳輸時間越短,JavaScript 引擎解析包的速度也越快。
那麼,Vue.js 3.0 在源碼體積的減少方面做了哪些工作呢?
-
移除一些冷門的 feature
比如 filter、inline-template 等
-
引入 tree-shaking 的技術,減少打包體積
Tree shaking 是一個通常用於描述移除 JavaScript 上下文中的未引用代碼(dead-code) 行為的術語。
它依賴於ES2015中的 import 和 export 語句,用來檢測代碼模塊是否被導出、導入,且被 JavaScript 文件使用。
在現代 JavaScript 應用程式中,我們使用模塊打包(如webpack或Rollup)將多個 JavaScript 文件打包為單個文件時自動刪除未引用的代碼。這對於準備預備發佈代碼的工作非常重要,這樣可以使最終文件具有簡潔的結構和最小化大小。
參考資料:
第一點很好理解,所以這裡我們來看看 tree-shaking,它的原理很簡單,tree-shaking 依賴 ES2015 模塊語法的靜態結構(即 import 和 export),通過編譯階段的靜態分析,找到沒有引入的模塊並打上標記。
舉個例子,一個 math 模塊定義了 2 個方法 square(x) 和 cube(x) :
export function square(x) {
return x * x
}
export function cube(x) {
return x * x * x
}
我們在這個模塊外面只引入了 cube 方法:
import { cube } from './math.js'
// do something with cube
最終 math 模塊會被 webpack 打包生成如下代碼:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
可以看到,未被引入的 square 模塊被標記了, 然後壓縮階段會利用例如 uglify-js、terser 等壓縮工具真正地刪除這些沒有用到的代碼。
也就是說,利用 tree-shaking 技術,如果你在項目中沒有引入 Transition、KeepAlive 等組件,那麼它們對應的代碼就不會打包,這樣也就間接達到了減少項目引入的 Vue.js 包體積的目的。
2.2.2 數據劫持優化
2.2.2.1 數據響應式
Vue.js 區別於 React 的一大特色是它的數據是響應式的,這個特性從 Vue.js 1.x 版本就一直伴隨著,這也是 Vue.js 粉喜歡 Vue.js 的原因之一。
DOM 是數據的一種映射,數據發生變化後可以自動更新 DOM,用戶只需要專註於數據的修改,沒有其餘的心智負擔。
在 Vue.js 內部,想實現這個功能是要付出一定代價的,那就是必須劫持數據的訪問和更新。
其實這點很好理解,當數據改變後,為了自動更新 DOM,那麼就必須劫持數據的更新,也就是說當數據發生改變後能自動執行一些代碼去更新 DOM。
那麼問題來了,Vue.js 怎麼知道更新哪一片 DOM 呢?
因為在渲染 DOM 的時候訪問了數據,我們可以對它進行訪問劫持,這樣就在內部建立了依賴關係,也就知道數據對應的 DOM 是什麼了。
以上只是大體的思路,具體實現要比這更複雜,內部還依賴了一個 watcher 的數據結構做依賴管理,參考下圖:
2.2.2.2 響應式實現方案
-
Vue.js 1.x 和 Vue.js 2.x 版本
Vue.js 1.x 和 Vue.js 2.x 內部都是通過 Object.defineProperty 這個 API 去劫持數據的 getter 和 setter,具體是這樣的:
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})
但這個 API 有一些缺陷
-
它必須預先知道要攔截的 key 是什麼,所以它並不能檢測對象屬性的添加和刪除。
var vm = new Vue({ data: { a: 1 } }) // `vm.a` 現在是響應式的 vm.b = 2 // `vm.b` 不是響應式的
儘管 Vue.js 為瞭解決這個問題提供了 $set 和 $delete 實例方法,但是對於用戶來說,還是增加了一定的心智負擔。
- 另外 Object.defineProperty 的方式還有一個問題,舉個例子,比如這個嵌套層級比較深的對象:
export default {
data: {
a: {
b: {
c: {
d: 1
}
}
}
}
}
由於 Vue.js 無法判斷你在運行時到底會訪問到哪個屬性,所以對於這樣一個嵌套層級較深的對象,如果要劫持它內部深層次的對象變化,就需要遞歸遍歷這個對象,執行 Object.defineProperty 把每一層對象數據都變成響應式的。毫無疑問,如果我們定義的響應式數據過於複雜,這就會有相當大的性能負擔。
-
Vue.js 3.x 版本
為瞭解決上述 2 個問題,Vue.js 3.0 使用了 Proxy API 做數據劫持,它的內部是這樣的:
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
}
})
由於它劫持的是整個對象,那麼自然對於對象的屬性的增加和刪除都能檢測到。
但要註意的是,Proxy API 並不能監聽到內部深層次的對象變化,因此 Vue.js 3.x 的處理方式是在 getter 中去遞歸響應式,這樣的好處是真正訪問到的內部對象才會變成響應式,而不是無腦遞歸,這樣無疑也在很大程度上提升了性能。Vue.js 3.x 中響應式的實現過程比較複雜,在此不展開講解。
2.2.3 編譯優化
最後是編譯優化,為了便於理解,我們先來看一張圖:
這是 Vue.js 2.x 從 new Vue 開始渲染成 DOM 的流程,上面說過的響應式過程就發生在圖中的 init 階段,另外 template compile to render function 的流程是可以藉助 vue-loader 在 webpack 編譯階段離線完成,並非一定要在運行時完成。
所以想優化整個 Vue.js 的運行時,除了數據劫持部分的優化,我們可以在耗時相對較多的 patch 階段想辦法,Vue.js 3.0 也是這麼做的,並且它通過在編譯階段優化編譯的結果,來實現運行時 patch 過程的優化。
通過數據劫持和依賴收集,Vue.js 2.x 的數據更新並觸發重新渲染的粒度是組件級的:
雖然 Vue 能保證觸發更新的組件最小化,但在單個組件內部依然需要遍歷該組件的整個 vnode 樹,舉個例子,比如我們要更新這個組件:
<template>
<div id="content">
<p class="text">static text</p>
<p class="text">static text</p>
<p class="text">{{message}}</p>
<p class="text">static text</p>
<p class="text">static text</p>
</div>
</template>
整個 diff 過程如圖所示:
可以看到,因為這段代碼中只有一個動態節點,所以這裡有很多 diff 和遍歷其實都是不需要的,這就會導致 vnode 的性能跟模版大小正相關,跟動態節點的數量無關,當一些組件的整個模版內只有少量動態節點時,這些遍歷都是性能的浪費。
而對於上述例子,理想狀態只需要 diff 這個綁定 message 動態節點的 p 標簽即可。
Vue.js 3.0 做到了,它通過編譯階段對靜態模板的分析,編譯生成了 Block tree。Block tree 是一個將模版基於動態節點指令切割的嵌套區塊,每個區塊內部的節點結構是固定的,而且每個區塊只需要以一個 Array 來追蹤自身包含的動態節點。藉助 Block tree,Vue.js 將 vnode 更新性能由與模版整體大小相關提升為與動態內容的數量相關,這是一個非常大的性能突破,此過程比較複雜。
除此之外,Vue.js 3.0 在編譯階段還包含了對 Slot 的編譯優化、事件偵聽函數的緩存優化,並且在運行時重寫了 diff 演算法等。
2.3 語法 API 優化:Composition API
除了源碼和性能方面,Vue.js 3.0 還在語法方面進行了優化,主要是提供了 Composition API。
2.3.1 優化邏輯組織
首先,是優化邏輯組織。
在 Vue.js 1.x 和 2.x 版本中,編寫組件本質就是在編寫一個“包含了描述組件選項的對象”,我們把它稱為 Options API,它的好處是在於寫法非常符合直覺思維,對於新手來說這樣很容易理解,這也是很多人喜歡 Vue.js 的原因之一。
Options API 的設計是按照 methods
、computed
、data
、props
這些不同的選項分類,當組件小的時候,這種分類方式一目瞭然;但是在大型組件中,一個組件可能有多個邏輯關註點,當使用 Options API 的時候,每一個關註點都有自己的 Options,如果需要修改一個邏輯點關註點,就需要在單個文件中不斷上下切換和尋找。
舉一個官方例子 Vue CLI UI file explorer,它是 vue-cli GUI 應用程式中的一個複雜的文件瀏覽器組件。這個組件需要處理許多不同的邏輯關註點:
- 跟蹤當前文件夾狀態並顯示其內容
- 處理文件夾導航(比如打開、關閉、刷新等)
- 處理新文件夾的創建
- 切換顯示收藏夾
- 切換顯示隱藏文件夾
- 處理當前工作目錄的更改
如果我們按照邏輯關註點做顏色編碼,就可以看到當使用 Options API 去編寫組件時,這些邏輯關註點是非常分散的:
Vue.js 3.0 提供了一種新的 API:Composition API,它有一個很好的機制去解決這樣的問題,就是將某個邏輯關註點相關的代碼全都放在一個函數里,這樣當需要修改一個功能時,就不再需要在文件中跳來跳去。
通過下圖,我們可以很直觀地感受到 Composition API 在邏輯組織方面的優勢:
2.3.2 優化邏輯復用
其次,是優化邏輯復用。
當我們開發項目變得複雜的時候,免不了需要抽象出一些復用的邏輯。在 Vue.js 2.x 中,我們通常會用 mixins 去復用邏輯,舉一個滑鼠位置偵聽的例子,我們會編寫如下函數 mousePositionMixin:
const mousePositionMixin = {
data() {
return {
x: 0,
y: 0
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
destroyed() {
window.removeEventListener('mousemove', this.update)
},
methods: {
update(e) {
this.x = e.pageX
this.y = e.pageY
}
}
}
export default mousePositionMixin
然後在組件中使用:
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>
使用單個 mixin 似乎問題不大,但是當我們一個組件混入大量不同的 mixins 的時候,會存在兩個非常明顯的問題:
- 命名衝突
- 數據來源不清晰
首先每個 mixin 都可以定義自己的 props、data,它們之間是無感的,所以很容易定義相同的變數,導致命名衝突。
另外對組件而言,如果模板中使用不在當前組件中定義的變數,那麼就會不太容易知道這些變數在哪裡定義的,這就是數據來源不清晰。
但是Vue.js 3.0 設計的 Composition API,就很好地幫助我們解決了 mixins 的這兩個問題。
我們來看一下在 Vue.js 3.0 中如何書寫這個示例:
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
這裡我們約定 useMousePosition 這個函數為 hook 函數,然後在組件中使用:
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import useMousePosition from './mouse'
export default {
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
}
</script>
可以看到,整個數據來源清晰了,即使去編寫更多的 hook 函數,也不會出現命名衝突的問題。
Composition API 除了在邏輯復用方面有優勢,也會有更好的類型支持,因為它們都是一些函數,在調用函數時,自然所有的類型就被推導出來了,不像 Options API 所有的東西使用 this。
另外,Composition API 對 tree-shaking 友好,代碼也更容易壓縮。
3.總結
作者:CherishTheYouth 出處:https://www.cnblogs.com/CherishTheYouth/ 聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。對於本博客如有任何問題,可發郵件與我溝通,我的QQ郵箱是:[email protected]以上就是Vue.js 3.x 大版本所做的優化,在實際項目開發中,Vue.js 3.x 相對於 Vue.js 2.x 來說,確實能帶來更好的開發體驗和較大的性能提升。