[譯] 製作 Vue 3 的過程

DIVMonster發表於2020-05-28


原文連結: https://increment.com/frontend/making-vue-3

在過去的一年裡,Vue 團隊一直在研究 Vue.js 的下一個主要版本,我們希望在 2020 年上半年釋出。這項工作在撰寫本文時還在進行中),關於 Vue 新的主要版本的想法是在 2018 年底形成的,當時 Vue 2 的程式碼庫大約有兩年半的時間。在通用軟體的生命週期中,這聽起來可能並不長,但在這段時間內,前端的格局發生了翻天覆地的變化。

有兩個關鍵的考慮因素促使我們對 Vue 進行了新的主要版本(和重寫)。第一,主流瀏覽器中新的 JavaScript 語言功能的普遍存在。第二,當前程式碼庫中暴露出來的設計和架構問題。

為什麼要重寫

利用新的語言功能

隨著 ES2015 的標準化,JavaScript(即 ECMAScript,縮寫為 ES 的縮寫)得到了重大改進,主流瀏覽器終於開始為這些新增加的功能提供了像樣的支援。特別是一些新新增的功能為我們提供了極大的機會,使 Vue 的能力得到了極大的提升。

其中最值得一提的是 Proxy,它允許框架對物件進行攔截操作。Vue 的一個核心特性是能夠監聽使用者定義狀態的變化,並對 DOM 進行反應式更新。Vue 2 通過用 getter 和 setter 替換狀態物件上的屬性,實現了這種反應性。切換到 Proxy 可以讓我們消除 Vue 現有的限制,比如無法檢測到新的屬性新增,並提供更好的效能。

然而,Proxy 是一個原生語言的功能,在傳統的瀏覽器中無法完全複用。為了利用它,我們知道我們必須調整框架的瀏覽器支援範圍--這是一個重大的突破性改變,只能在新的主要版本中提供。

解決架構問題

在現有的程式碼庫中修復這些問題,需要進行巨大的、危險的重構,幾乎相當於重寫。
在維護 Vue 2 的過程中,由於現有架構的限制,我們已經積累了很多問題,這些問題很難解決。例如,模板編譯器的編寫方式使得適當的原始碼對映支援變得非常具有挑戰性。此外,雖然 Vue 2 在技術上可以構建針對非 DOM 平臺的更高階別的渲染器,但我們不得不對程式碼庫進行分叉,並重復了大量的程式碼來實現這一點。在當前的程式碼庫中修復這些問題,將需要進行巨大的、危險的重構,幾乎相當於重寫。

同時,我們還以各種模組內部的隱式耦合形式積累了技術債務,以及似乎不屬於任何地方的浮動程式碼。這使得我們很難孤立地理解程式碼庫中的某一部分,而且我們注意到,貢獻者很少有信心進行一些無關緊要的修改。重寫將給我們提供了一個機會,讓我們在考慮到這些問題的情況下重新思考程式碼組織。

初步原型設計階段

我們在 2018 年底開始了 Vue 3 的原型開發,初步目標是驗證這些問題的解決方案。在這個階段,我們主要集中在為進一步開發打下堅實的基礎上。

切換到 typescript

Vue 2 最初是用普通的 ES 編寫的。在原型設計階段後不久,我們意識到型別系統對於這樣的專案來說是非常有幫助的。型別檢查大大減少了在重構過程中引入意外 bug 的機會,並幫助貢獻者更有信心地進行簡單的修改。我們採用了 Facebook 的 Flow 型別檢查器,因為它可以逐步新增到現有的純 ES 專案中。Flow 在一定程度上起到了一定的幫助,但我們並沒有如願以償地受益;特別是,不斷的破壞性修改讓升級成為一種痛苦。與 TypeScript 與 Visual Studio Code 的深度整合相比,對整合開發環境的支援也並不理想。

我們還注意到,使用者越來越多地將 Vue 和 TypeScript 一起使用。為了支援他們的用例,我們不得不將 TypeScript 宣告與原始碼分開編寫和維護,而原始碼使用的是不同的型別系統。轉換到 TypeScript 將使我們能夠自動生成宣告檔案,減輕了維護負擔。

內部包的解耦

我們還採用了一個單體化的設定,框架由內部包組成,每個包都有自己的 API、型別定義和測試。我們希望讓這些模組之間的依賴關係更加明確,讓開發者更容易閱讀、理解和修改。這對於我們努力降低專案的貢獻障礙和提高專案的長期可維護性是非常關鍵的。

設定 RFC 流程

到 2018 年年底,我們已經有了一個工作原型,有了新的反應式系統和虛擬 DOM 渲染器。我們已經驗證了我們想做的內部架構改進,但只有面向公眾的 API 改動的粗略草稿。現在是時候把它們變成具體的設計了。

我們知道我們必須儘早、謹慎地完成這項工作。Vue 的廣泛使用意味著破壞性的改變可能會導致使用者的大量遷移成本和潛在的生態系統碎片化。為了確保使用者能夠提供對打破性改動的反饋,我們在 2019 年初採用了 RFC(徵求意見)流程。每個 RFC 都遵循一個模板,其中的章節集中在動機、設計細節、權衡和採用策略等方面。由於該流程是在 GitHub repo 中進行的,建議以拉動請求的形式提交,因此討論在評論中有機地展開。

事實證明,RFC 流程非常有幫助,它作為一個思想框架,迫使我們充分考慮到了潛在變革的所有方面,並允許我們的社群參與到設計過程中,提交深思熟慮的功能請求。

更快、更小

效能對於前端框架來說是至關重要的。儘管 Vue 2 擁有極具競爭力的效能,但通過實驗新的渲染策略,重寫提供了一個更進一步的機會。

克服虛擬 DOM 的瓶頸

Vue 有一個相當獨特的渲染策略。它提供了一個類似於 HTML 的模板語法,但將模板編譯成了返回虛擬 DOM 樹的渲染函式。該框架通過遞迴地走過兩個虛擬 DOM 樹,並比較每個節點上的每一個屬性,計算出實際 DOM 的哪些部分需要更新。由於現代 JavaScript 引擎進行了高階優化,這種有點蠻力的演算法一般來說是相當快的,但是更新仍然會涉及到很多不必要的 CPU 工作。當你看一個基本上是靜態內容和只有幾個動態繫結的模板時,效率低下的問題就特別明顯--整個虛擬 DOM 樹仍然需要遞迴地走一遍,以找出改變了什麼。

幸運的是,模板編譯步驟讓我們有機會對模板進行靜態分析並提取動態部分的資訊。Vue 2 通過跳過靜態子樹在一定程度上做到了這一點,但由於編譯器架構過於簡單化,更高階的優化很難實現。在 Vue 3 中,我們用適當的 AST transform pipeline 重寫了編譯器,這使得我們可以用變換外掛的形式來進行編譯時的優化。

有了新的架構,我們希望找到一種能夠儘可能消除開銷的渲染策略。一個選擇是拋棄虛擬 DOM,直接生成必要的 DOM 操作,但這將消除直接編寫虛擬 DOM 渲染函式的能力,我們發現這對高階使用者和庫作者來說是非常有價值的。另外,這將是一個巨大的突破性改變。

接下來最好的辦法就是去掉不必要的虛擬 DOM 樹遍歷和屬性比較,這些往往在更新過程中的效能開銷最大。為了實現這個目標,編譯器和執行時需要協同工作。編譯器分析模板,並生成帶有優化提示的程式碼,而執行時則接收這些提示並儘可能地採取快速路徑。這裡有三個主要的優化工作。

首先,在樹級,我們注意到,在沒有模板指令動態改變節點結構的情況下,節點結構保持完全靜態(例如,v-if 和 v-for)。如果我們把一個模板劃分成由這些結構指令分隔的巢狀 "塊",那麼每個塊內的節點結構又變得完全靜態。當我們更新塊內的節點時,我們不再需要遞迴地遍歷塊內的樹形動態繫結,可以在一個平面陣列中跟蹤。這種優化避免了虛擬 DOM 的大部分開銷,減少了我們需要執行的樹狀遍歷量,減少了一個數量級。

其次,編譯器會主動檢測模板中的靜態節點、子樹,甚至是資料物件,並在生成的程式碼中把它們掛在渲染函式之外。這就避免了在每次渲染時重新建立這些物件,極大地提高了記憶體使用量,減少了垃圾回收的頻率。

第三,在元素層面,編譯器還會根據每個具有動態繫結的元素需要執行的更新型別,為其生成一個優化標誌。例如,一個具有動態類繫結和多個靜態屬性的元素將收到一個標誌,提示只需要進行類檢查。執行時將接收到這些提示並採取專用的快速路徑。

CPU 時間 即執行 JavaScript 計算所花費的時間,不包括瀏覽器 DOM 操作。

綜合起來,這些技術大大改善了我們的渲染更新基準,Vue 3 有時只需要不到 Vue 2 的十分之一的 CPU 時間。

最大限度地減少軟體包的大小

框架的大小也會影響到它的效能。這對於 Web 應用來說是一個獨特的問題,因為資產需要實時下載,在瀏覽器解析了必要的 JavaScript 之後,應用程式才會進行互動。這對於單頁面應用來說尤其如此。雖然 Vue 一直以來都是相對輕量級的--Vue 2 的執行時大小約為 23 KB gzipped,但我們注意到了兩個問題。

首先,不是每個人都會使用該框架的所有功能。例如,一個從未使用過過渡功能的應用仍然要支付過渡相關程式碼的下載和解析成本。

第二,隨著我們新增新的功能,框架不斷地無限增長。當我們考慮增加新功能的權衡時,這就賦予了捆綁大小不成比例的權重。因此,我們傾向於只包含大多數使用者會使用的功能。

理想的情況下,使用者應該能夠在構建的時候,為未使用的框架功能丟棄程式碼,也就是所謂的 "動搖樹"--只為他們使用的功能付費。這也可以讓我們在不增加其他使用者的付費成本的情況下,為一部分使用者提供有用的功能。

在 Vue 3 中,我們通過將大部分的全域性 API 和內部幫助器轉移到 ES 模組匯出中來實現了這一點。這使得現代捆綁器能夠靜態分析模組的依賴性,並丟棄與未使用的匯出相關的程式碼。模板編譯器還生成了樹狀的友好程式碼,只有在模板中實際使用某個功能的時候才會匯入幫助器。

框架中的一些部分永遠不能被樹形動搖,因為它們對於任何型別的 app 都是必不可少的。我們把這些必不可少的部分的衡量標準稱為基線大小。Vue 3 的基線大小約為 10KB 左右,儘管增加了許多新功能,但它的基線大小還不到 Vue 2 的一半。

解決規模化的需求

我們還希望提高 Vue 的處理大規模應用的能力。我們最初的 Vue 設計的重點是低准入門檻和溫和的學習曲線。但隨著 Vue 的應用越來越廣泛,我們更多的瞭解到了包含數百個模組的專案的需求,並且由幾十個開發人員長期維護的專案。對於這些型別的專案來說,像 TypeScript 這樣的型別系統和乾淨利落地組織可重用程式碼的能力是至關重要的,而 Vue 2 在這些方面的支援並不理想。

在設計 Vue 3 的早期階段,我們試圖通過提供對使用類編寫元件的內建支援來改進 TypeScript 的整合。我們所面臨的挑戰是,我們需要的許多語言特性,如類欄位和裝飾符等,在正式成為 JavaScript 的一部分之前,仍然是建議--而且可能會發生變化。所涉及的複雜性和不確定性讓我們懷疑增加類 API 是否真的合理,因為它除了稍微好一點的 TypeScript 整合之外,並沒有提供其他任何東西。

我們決定研究其他方式來攻擊縮放問題。受 React Hooks 的啟發,我們想到了暴露出底層的反應性和元件生命週期 API,以實現一種更自由的方式來編寫元件邏輯,稱為 Composition API。與其通過指定一長串選項來定義一個元件,Composition API 允許使用者像寫函式一樣自由地表達、編譯和重用有狀態的元件邏輯,同時提供了出色的 TypeScript 支援。

我們對這個想法非常興奮。雖然 Composition API 是為了解決特定類別的問題而設計的,但在技術上,只有在編寫元件時才可以使用它。在提案的第一稿中,我們有點超前了,並暗示我們可能會在未來的版本中用 Composition API 替換現有的 Options API。這導致了社群成員的巨大反擊,這給我們上了寶貴的一課,讓我們明白瞭如何清晰地傳達長期計劃和意圖,以及瞭解使用者的需求。在聽取了社群成員的反饋後,我們對提案進行了徹底的修改,明確了 Composition API 將是對 Options API 的補充和補充。修改後的提案得到了更多的好評,並收到了很多建設性的建議。

尋求平衡

開發者簡介的多樣性與用例的多樣性相對應。
在 Vue 的使用者群中,超過 100 萬的開發者中,有隻懂 HTML/CSS 的初學者,也有從 jQuery 遷移過來的專業人士,有從其他框架遷移過來的老手,有尋找前端解決方案的後端工程師,也有處理規模化軟體的軟體架構師。開發者配置檔案的多樣性與用例的多樣性相對應。一些開發人員可能希望在傳統的應用程式上灑上互動性,而另一些開發人員可能是一次性的專案,週轉速度快,但維護問題有限;架構師可能需要處理大型的、多年期的專案,並且在專案的生命週期內,開發人員的團隊起伏不定。

Vue 的設計一直在不斷地塑造和了解這些需求,在各種權衡中尋求平衡。Vue 的口號是 "漸進式框架",這句話概括了這個過程中產生的分層 API 設計。初學者可以通過 CDN 指令碼、基於 HTML 的模板和直觀的 Options API 享受平穩的學習曲線,而專家們可以通過全功能的CLI、渲染函式和 Composition API 來處理巨集大的用例。

為了實現我們的願景,還有很多工作要做--最重要的是,更新支援庫、文件和工具以確保順利遷移。我們將在未來的幾個月裡努力工作,我們迫不及待地想看看社群將用 Vue 3 創造出什麼。

相關文章