木木剑光

mmjg

Full Stack Developer | Ant design Contributor

Vapor模式研究計劃前奏:淺析架構演進史

什麼是 Vapor mode#

Vapor mode 中文直譯是 蒸汽模式

這是從 Vapor 的 github 倉庫中截的一段描述

image

為了開發 Vapor mode,vue 團隊從 vue3 的主線分支 fork 出了一個新的倉庫「core-vapor」,目標是為了實現 vue 的 無虛擬 DOM 渲染模式

這裡對 Vue 發展過程比較了解的同學可能就會發問了,「Vue1.0」 不就是無虛擬 DOM 的版本嗎,發展了這麼多年怎麼還往回走呢?說到這裡我們就不得不梳理一下 Vue 各個大版本的 「架構演進史」

Vue 1.0 量子纏結式的細粒度綁定#

這是 Vue 初具雛形的一個里程碑,此時還沒有完備的 響應式 系統,這個時期大家談論得最多的是基於 數據劫持 + 依賴收集 的響應式實現方案,以及實現細節中的 WatcherDep 兩兄弟

沒有了解過的同學可以看一下這張「渲染流程圖」,能幫助你快速建立起對 Vue1.0 版本的認知

image

在還沒有 SFC(單文件組件)之前,Vue 可以說是一個實打實的 「運行時」 框架,上圖簡要體現了當我們 new Vue({...}) 之後,發生的一系列過程

  • defineProperty 實現數據劫持,目的是代理數據,攔截所有對數據的 getset 操作,這個過程也被稱為「getter/setter」化

  • 屬性的「getter」的主要作用是收集依賴。這裡的「依賴」是指所有訪問該屬性的邏輯,可能是一個「計算屬性」,可能是一個「監聽器」,也可能是模版中的「指令」或者「插值表達式」。在實現層面,這些不同的依賴都被抽象為了一個統一的概念 —— Watcher,會被存儲在每個屬性的 Dep 實例中

  • 屬性的「setter」則是當數據被修改後,通知 Dep 中收集的 Watcher 去更新視圖

在整個流程中,Vue 依賴收集的 「粒度」是很細的,只要是訪問了「響應式」數據的地方,都會被作為「依賴」 給收集起來

往上層說,也就是 「數據」與對應的「UI」形成了綁定關係,這也是讓 Vue 在「更新視圖」時有著極高的效率的根本原因。這種綁定關係就像量子纏結一樣,「數據」變化的同時「UI」 就能收到通知從而做出更新

但任何事物都有兩面性,過細的依賴收集是優勢也是短板,隨著項目體量的增長,運行時會產生越來越多的 WatcherDep,導致佔用過多的內存,進而影響頁面的性能。

如果一個方案只能支撐小體量的項目有良好的 「性能」 表現,顯然不是「最優解」

Vue 2.0 調整依賴收集粒度,引入虛擬 DOM#

為了在大型項目上有更好的性能表現,Vue2.0 版本做出了很大的調整。從下面的流程圖中,我們不難發現 Vue 的架構體系有了直觀的變化

  • 相比 1.0 版本,出現了組件的概念
  • 將依賴收集的粒度調整為「組件級別」,即一個組件就是一個 Watcher
  • 引入了 虛擬 DOM 並且成為了渲染流程中非常重要的一環

image

整體上來看,「響應式」數據不再關注組件內部的「依賴」,當數據被修改後只通知到組件,組件再通過 diff 算法找出「虛擬 DOM」中變化部分,將這部分更新到「真實 DOM」上。這本質上是 時間換空間 的權衡,通過適當降低 「運行時」「更新效率」 來換取更少的 「內存開銷」

同時期 Vue 也新增了 SFC(.vue 文件),由於 .vue 文件是框架提供的“魔法”,並不能直接交給瀏覽器去執行,因此就需要一個能打破“魔法”的工具,將 .vue 轉換為 .js,這個工具的核心就是 編譯器(compiler),打破魔法的過程則被稱為 編譯(compile),也就是說經過編譯後,我們在 .vue 文件中寫的 v-if, v-for, 插值表達式 等等特性都會變成普通的 javascript 邏輯。template 也就是在這個階段被轉換為了 render 函數

image

雖然 1.0 版本中也存在 compile 過程,但做的事兒卻發生了很大的變化

compile1.02.0
階段運行時編譯時
作用將模板中的指令,插值等解析為對應的邏輯並執行將模板解析為抽象語法樹 (AST),生成渲染函數

通過簡單的對比我們不難發現,儘管編譯的執行階段完全不同了,對模板的解析還是主旋律

其實到這裡,是不是感覺 Vue 已經從架構上做到了極致的優化?不要著急,我們看完 Vue 3.0 版本的演進後再下論斷

Vue 3.0 組合式 API#

隨著 typescript 在前端的興起,以及代表著 元編程 能力的 proxy 被各大主流瀏覽器所支持,Vue3 以被完全重構的姿態登場了

同時,為了解決 選項式API 邏輯太分散,開發者難以優雅的寫出高內聚的代碼 這一痛點,在得到 React hooks 思想的啟發後,Vue3 也推出了 組合式 API,雖然期間也有 mixin 之類的方案,但或多或少都有弊端

而在 組合式 API 模式下,利用 hooks 能夠很好的組合業務功能,實現邏輯內聚

除了新特性之外,在細節的打磨上,Vue 也做了很多優化,比如

  • 預字符串化
  • 靜態節點提升
  • patch flag

細心的同學應該已經發現了,這些優化都是屬於 編譯時 優化,而不是 運行時 優化,因為在運行時,可優化的方向實在不多,基本是圍繞 patch 過程的 diff 算法來進行的

再說 Vue vapor#

在回顧了 Vue 的迭代過程後,我們就可以回到主題 Vapor mode 了,這也是 Vue 圍繞 編譯時優化 進行的一項研究,當然也是受到了 SolidJS 的啟發

前面我們提到,基於的 Vue 的架構體系,運行時優化 幾乎只能圍繞如何更快的找出 虛擬 DOM 中變化的部分來做

如果回到 Vue1.0 沒有虛擬 DOM的形態,那個時候的細粒度綁定就不需要去找變化,更不需要虛擬 DOM,但痛點是

  • 依賴是在運行時確定的。系統初始化需要在編譯階段解析指令等並收集依賴,會增加首屏渲染的時間

  • 細粒度的依賴收集導致產生大量的 Watcher,帶來更多的內存開銷

現在我們換一個視角,如果把依賴的確定時機運行時挪到編譯時,通過編譯手段將每個 響應式數據 變化時需要執行的更新邏輯生成出來,不就能解決上述的第一個痛點了嗎,這也就是 Vapor mode 的思路

至於第二個痛點,從理論上來說,依賴收集的粒度由粗到細,勢必會增加內存開銷,這個點 Vapor mode 將如何處理?我們存疑,等它正式發布後再 callback

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。