[譯] 制作 Vue 3 的過程



原文鏈接: 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 創造出什么。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM