本文整理自拉勾網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 是做不到這一點的,
參考資料:
- 什么是monorepo?
- Vue3.0 中的 monorepo 管理模式
- 現代前端工程為什么越來越離不開 Monorepo?
2.1.2 有型別的 JavaScript:TypeScript
原始碼的優化還體現在 Vue.js 3.0 自身采用了 TypeScript 開發,
Vue.js 1.x 版本的原始碼是沒有用型別語言的,祖師爺用 JavaScript 開發了整個框架,但對于復雜的框架專案開發,使用型別語言非常有利于代碼的維護,因為它可以在編碼期間幫你做型別檢查,避免一些因型別問題導致的錯誤;也可以利于它去定義介面的型別,利于 IDE 對變數型別的推導,
因此在重構 2.0 的時候,祖師爺選型了 Flow(Flow是JavaScript代碼的靜態型別檢查器,),
參考資料:
- Flowjs官方檔案
- Flow和TypeScript之間的區別和優劣
但是在 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,它的原理很簡單,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 實體方法,但是對于用戶來說,還是增加了一定的心智負擔,
Vue 2 中更改檢測的注意事項——$set
- 另外 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 >static text</p>
<p >static text</p>
<p >{{message}}</p>
<p >static text</p>
<p >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 = https://www.cnblogs.com/CherishTheYouth/archive/2022/04/21/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 來說,確實能帶來更好的開發體驗和較大的性能提升,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/460859.html
標籤:其他
