《Vue3 核心源碼解析》總結
Vue3 優化
Vue.js 從 1.x 到 2.0 版本,最大的升級就是引入了虛擬 DOM 的概念, Vue.js 2.x 發展了很久,現在周邊的生態設施都已經非常完善了,但是還是存在不少痛點:比如源碼自身的維護性、數據量大后帶來的渲染和更新的性能問題、一些想舍棄但為了兼容一直保留的雞肋 API 等;框架開發者希望能給普通開發人員帶來更好的編程體驗,比如更好的 TypeScript 支持、更好的邏輯復用實踐等,於是從源碼、性能和語法 API 三個大的方面優化框架。
源碼
源碼優化,也是對 Vue 框架本身開發的優化,主要目的是讓代碼更易於開發和維護。源碼的優化主要體現在使用 monorepo 和 TypeScript 管理和開發源碼。
采用 monorepo 代碼管理方式
Vue3,整個源碼是通過 monorepo 的方式維護的,根據功能將不同的模塊拆分到 packages 目錄下面不同的子目錄中:

相對於 Vue.js 2.x 的源碼組織方式,monorepo 把這些模塊拆分到不同的 package 中,每個 package 有各自的 API、類型定義和測試。這樣使得模塊拆分更細化,職責划分更明確,模塊之間的依賴關系也更加明確,開發人員也更容易閱讀、理解和更改所有模塊源碼,提高代碼的可維護性。
引入 TypeScript
源碼的優化還體現在 Vue.js 3.0 自身采用了 TypeScript 開發。對於復雜的框架項目開發,使用類型語言非常有利於代碼的維護,因為它可以在編碼期間幫你做類型檢查,避免一些因類型問題導致的錯誤;也可以利於它去定義接口的類型,利於 IDE 對變量類型的推導。
TypeScript 提供了更好的類型檢查,能支持復雜的類型推導;由於源碼就使用 TypeScript 編寫,也省去了單獨維護 d.ts 文件的麻煩;
性能
源碼體積
靜態資源體積優化,JavaScript 包體積越小,意味着網絡傳輸時間越短,JavaScript 引擎解析包的速度也越快。
- 移除一些冷門的 feature(比如 filter、inline-template 等);
- 引入 tree-shaking 的技術,減少打包體積。
數據劫持
Vue.js 1.x 和 Vue.js 2.x 內部都是通過 Object.defineProperty 這個 API 去劫持數據的 getter 和 setter,此 API 存在一些問題:
- 要想使用此 API,必須預先知道要攔截的 key 是什么,所以並不能檢測對象屬性的添加和刪除。Vue.js 為了解決這個問題提供了 $set 和 $delete 實例方法。
- Object.defineProperty 對於一個嵌套層級較深的對象,如果要劫持它內部深層次的對象變化,就需要遞歸遍歷這個對象,執行 Object.defineProperty 把每一層對象數據都變成響應式的,如果我們定義的響應式數據過於復雜,這就會有相當大的性能負擔。
為了解決上述 2 個問題,Vue.js 3.0 使用了 Proxy API 做數據劫持
observed = new Proxy(data, {
get() {
// track
},
set() {
// trigger
},
})
Proxy API 並不能監聽到內部深層次的對象變化,Vue3 在 getter 中去遞歸響應式,真正訪問到的內部對象才會變成響應式
編譯過程
Vue3 通過編譯階段對靜態模板的分析,編譯生成了 Block tree。Block tree 是一個將模版基於動態節點指令切割的嵌套區塊,每個區塊內部的節點結構是固定的,而且每個區塊只需要以一個 Array 來追蹤自身包含的動態節點。
Vue3 在編譯階段還包含了對 Slot 的編譯優化、事件偵聽函數的緩存優化,並且在運行時重寫了 diff 算法
語法
邏輯組織
Vue1.x 與 Vue2.x Options API 的設計是按照 methods、computed、data、props 這些不同的選項分類,當組件小的時候,這種分類方式一目了然;但是在大型組件中,一個組件可能有多個邏輯關注點,當使用 Options API 的時候,每一個關注點都有自己的 Options,如果需要修改一個邏輯點關注點,就需要在單個文件中不斷上下切換和尋找。
Vue3 Composition API,它有一個很好的機制去解決這樣的問題,就是將某個邏輯關注點相關的代碼全都放在一個函數里

邏輯復用
定義自定義 hook 函數,在組件中使用
Composition API 除了在邏輯復用方面有優勢,也會有更好的類型支持,因為它們都是一些函數,在調用函數時,自然所有的類型就被推導出來了,不像 Options API 所有的東西使用 this。另外,Composition API 對 tree-shaking 友好,代碼也更容易壓縮。
核心實現
虛擬 DOM 渲染
組件是一個抽象的概念,它是對一棵 DOM 樹的抽象,組件的渲染取決於組件的模板
組件的模板決定了組件生成的 DOM 標簽,而在 Vue.js 內部,一個組件想要真正的渲染生成 DOM,還需要經歷“創建 vnode - 渲染 vnode - 生成 DOM” 這幾個步驟
- 應用程序初始化:創建 app 對象和重寫 app.mount 方法
const createApp = (...args) => {
// 創建 app 對象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重寫 mount 方法
app.mount = containerOrSelector => {
// ...
}
return app
}
-
創建 vnode:組件 vnode 其實是對抽象事物的描述,將 DOM 使用 JavaScript 對象來描述,可以描述不同類型的節點,比如普通元素節點、組件節點等
-
渲染 vnode:渲染組件生成 subTree、把 subTree 掛載到 container 中。通過 reder 函數創建組件樹 vnode,把這個 vnode 再經過內部一層標准化,得到子樹 vnode
-
掛載元素函數:創建 DOM 元素節點、處理 props、處理 children、掛載 DOM 元素到 container 上

diff 流程
在已知舊子節點的 DOM 結構、vnode 和新子節點的 vnode 情況下,以較低的成本完成子節點的更新為目的,求解生成新子節點 DOM 的系列操作。
-
同步頭部節點

同步頭部節點就是從頭部開始,依次對比新節點和舊節點,如果它們相同的則執行 patch 更新節點;如果不同或者索引 i 大於索引 e1 或者 e2,則同步過程結束。 -
同步尾部節點

同步尾部節點就是從尾部開始,依次對比新節點和舊節點,如果相同的則執行 patch 更新節點;如果不同或者索引 i 大於索引 e1 或者 e2,則同步過程結束。
接下來只有 3 種情況要處理:
- 新子節點有剩余要添加的新節點;
- 舊子節點有剩余要刪除的多余節點;
- 未知子序列。
- 添加新節點

如果索引 i 大於尾部索引 e1 且 i 小於 e2,那么從索引 i 開始到索引 e2 之間,直接掛載新子樹這部分的節點。
添加完 e 節點后,舊子節點的 DOM 和新子節點對應的 vnode 映射一致,也就完成了更新。
- 刪除多余節點
-
從頭部同步節點:

-
接着從尾部同步節點:

-
刪除子節點中的多余節點:

-
舊子節點的 DOM 和新子節點對應的 vnode 映射一致,也就完成了更新。
