[譯]尤雨溪:Vue3的設計過程

語言: CN / TW / HK

今日凌晨三點半左右,尤雨溪在他的微博上發表了一篇文章。 當然大佬是在另一個時區,咱們這的凌晨對應的應該是那個疫情最嚴重的時區的下午。

原文連結:increment.com/frontend/ma…

重構新版Vue.js的經驗教訓

在過去的一年中,Vue團隊一直在研究Vue.js的下一個主要版本,我們希望能在2020年的上半年釋出該版本。(在撰寫本文時,這項工作仍在進行中)。 Vue的主要版本於2018年底形成,當時Vue 2的程式碼庫已有兩年半的歷史了。在通用軟體的生命週期中聽起來可能並不長,但在此期間,前端環境發生了巨大變化。

有兩個主要的考慮因素使我們開發了Vue的新主要版本(並重寫了它):首先,主流瀏覽器普遍提供了新的JavaScript語言功能。其次,隨著時間的推移,當前程式碼庫中的設計和體系結構問題已經逐漸暴露了出來。

為什麼要重構

利用新的語言功能

隨著ES6的標準化,JavaScript(正式稱為ECMAScript,縮寫為ES)獲得了重大改進,主流瀏覽器終於開始為這些新功能提供不錯的支援。特別是一些新特性為我們提供了極大提高Vue功能的機會。

其中最值得注意的是Proxy,它允許框架攔截物件上的操作。Vue的核心功能是能夠監聽對使用者定義狀態所做的更改並以資料驅動更新DOM的能力。Vue 2通過使用getter和setter替換狀態物件上的屬性來實現這種能力。切換到代理將使我們消除Vue的現有限制,例如無法檢測到新的屬性新增並提供更好的效能。

但是,代理是語言自身的功能,不能在舊版瀏覽器中完全polyfill。為了利用它,我們知道我們必須調整框架的瀏覽器支援範圍,這是一個重大突破,只能在新的主要版本中釋出。

解決架構問題

要在當前的程式碼庫中解決這些問題,將需要進行大量風險較大的重構,這幾乎等同於完全重寫了一遍 。 在維護Vue 2的過程中,由於現有架構的侷限性,我們積累了許多難以解決的問題。例如,模板編譯器的編寫方式令源對映的支援非常困難。同樣,雖然Vue 2從技術上允許構建針對非DOM平臺的更高級別的渲染器,但我們必須派生程式碼庫並複製大量程式碼才能實現這一點。要在當前的程式碼庫中解決這些問題,將需要進行大量風險較大的重構,這幾乎等同於完全重寫 。

同時,我們以各種模組的內部與浮動程式碼之間隱式耦合的形式積累了隱患,而浮動程式碼似乎並不屬於任何地方。這使得孤立地理解程式碼庫的一部分變得更加困難,並且我們注意到,貢獻者很少會對重要的更改充滿信心。重構將使我們有機會根據這些注意事項來重新考慮程式碼組織。

初始原型階段

我們於2018年底開始對Vue 3進行原型設計,其初步目標是驗證這些問題的解決方案。在此階段,我們主要為進一步發展奠定堅實的基礎。

切換到TypeScript

Vue 2最初是用純JS編寫的。在原型開發階段之後不久,我們意識到型別系統對於這種規模的專案將非常有幫助。型別檢查極大地減少了在重構期間引入意外錯誤的機會,並有助於貢獻者更自信的進行重要的更改。我們通過Facebook的Flow型別檢查,因為它可以逐步新增到現有的JS專案。Flow在一定程度上有所幫助,但是我們沒有從中獲得太多收益。特別是不斷變化的變化使升級變得很痛苦。與TypeScript和Visual Studio Code的深度整合相比,Flow對整合開發環境的支援也不理想。

我們還注意到,使用者越來越多地同時使用Vue和TypeScript。為了支援它們的用例,我們必須與使用不同型別系統分開創作和維護TypeScript宣告。切換到TypeScript將使我們能夠自動生成宣告檔案,從而減輕了維護負擔。

解耦內部封裝

我們還採用了monorepo,其中的框架由內部軟體包組成,每個內部軟體包都具有自己的單獨API,型別定義和測試。我們希望使這些模組之間的依賴關係更加明確,從而使開發人員更容易閱讀,理解並進行所有更改。這是我們努力降低專案貢獻壁壘並提高其長期可維護性的關鍵。

設定RFC流程

到2018年底,我們有了一個使用新的資料驅動檢視系統和虛擬DOM渲染器的工作原型。我們已經驗證了我們想要進行的內部體系結構改進,但是隻包含了面向公眾的API更改的草案。現在是將它們變成具體設計的時候了。

我們知道我們必須儘早而仔細地做到這一點。Vue的廣泛使用意味著突破性變化可能導致使用者大量遷移成本和潛在的生態系統碎片化。為了確保使用者能夠提供有關重大更改的反饋,我們在2019年初採用了RFC(徵求意見)流程。每個RFC遵循一個模板,其中側重於動機,設計細節,權衡和採用策略。由於此過程是在GitHub倉庫中進行的,提案是作為請求請求提交的,因此討論會在註釋中自然展開。

該RFC的過程已經證明了極大的幫助,作為一個思想框架,它迫使我們要充分考慮潛在變化的方方面面,讓我們的社群參與設計過程,並提交深思熟慮出來的功能要求。

更快更小

效能對於前端框架至關重要。儘管Vue 2具有出色的效能,但通過嘗試新的渲染策略,重構提供了進一步發展的可能。

克服虛擬DOM的瓶頸

Vue有一個相當獨特的呈現策略:它提供類似於HTML的模板語法,但將模板編譯為可返回虛擬DOM樹的呈現函式。該框架通過遞迴遍歷兩個虛擬DOM樹並比較每個節點上的每個屬性來確定實際DOM的哪些部分需要更新。由於現代JavaScript引擎執行了高階優化,因此這種有點野蠻的演算法通常很快,但是仍然涉及許多不必要的CPU工作。當您檢視包含大量靜態內容且只有少量動態繫結(整個虛擬DOM)的模板時,效率低下,尤其僅有一點改變卻仍然需要遞迴整個虛擬DOM樹,以瞭解發生了什麼變化。

幸運的是,模板編譯步驟使我們有機會對模板進行靜態分析並提取有關動態零件的資訊。Vue 2通過跳過靜態子樹在某種程度上做到了這一點,但是由於過於簡單的編譯器體系結構,難以實施更高階的優化。在Vue 3中,我們使用適當的AST轉換管道重寫了編譯器,這使我們能夠以轉換外掛的形式編寫編譯時優化。

有了新的體系結構,我們希望找到一種渲染策略,以儘可能減少開銷。一種選擇是放棄虛擬DOM並直接生成命令式DOM操作,但這將消除直接編寫虛擬DOM渲染功能的能力,我們發現這對高階使用者和庫作者非常有價值。另外,這將是一個巨大的突破性變化。

其次,最好的方法是消除不必要的虛擬DOM樹遍歷和屬性比較,這在更新過程中往往會帶來最大的效能開銷。為了實現這一點,編譯器和執行時需要協同工作:編譯器分析模板並生成帶有優化提示的程式碼,而執行時將拾取提示並在可能的情況下采用快速路徑。這裡有三個主要的優化工作:

首先,在樹的層面上,我們注意到,節點結構在沒有模板指令的時候是完全靜態的(例如,v-if和v-for)。如果我們將模板分為動態的和靜態的“塊”,每個塊內的節點結構再次變得完全靜態。當我們更新一個塊內的節點時,我們不再需要遞迴遍歷樹,因為我們可以在平面陣列中跟蹤該塊內的動態繫結。通過將我們需要執行的樹遍歷量減少一個數量級,從而節約了虛擬DOM的大部分開銷。

其次,編譯器會主動檢測模板中的靜態節點,子樹甚至資料物件,並將其提升到生成程式碼中的render函式之外。這樣可以避免在每個渲染上重新建立這些物件,從而大大提高了記憶體使用率並減少了垃圾回收的頻率。

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

綜上所述,這些技術已顯著提高了我們的渲染更新,執行Vue 3有時甚至會比Vue 2快上個十倍。

極小的尺寸

框架的大小也會影響其效能。這是Web應用程式的重要關注點,因為需要動態下載資產,並且在瀏覽器解析必要的JavaScript之前,該應用程式將是互動式的。對於單頁應用程式尤其如此。儘管Vue一直是相對輕量級的(Vue 2的執行時大小壓縮為23 KB),但我們注意到了兩個問題:

首先,並不是每個人都使用框架的所有功能。例如,從未使用過渡元件的應用仍會下載與過渡相關的程式碼和並且花時間去解析它。

其次,當我們新增新功能時,該框架會無限的變大。當我們考慮新功能新增的時候,不得不考慮到尺寸的問題。因此,我們傾向於框架僅包含大多數使用者會使用的功能。

理想情況下,使用者應該能夠在構建時刪除未使用的框架功能的程式碼-也稱為“Tree Shaking” -只打包他們使用的程式碼。這也將使我們能夠釋出一部分使用者會覺得有用的功能,而不會增加其餘使用者的有效下載成本。

在Vue 3中,我們通過將大多數全域性API和內部幫助程式移至ES模組匯出來實現了這一目標。這使現代打包工具可以靜態分析模組依賴性並刪除與未使用的匯出相關的程式碼。模板編譯器還會生成Tree Shaking友好的程式碼,如果該功能實際上在模板中使用,則該程式碼僅匯入該功能的幫助程式。

框架的某些部分永遠不會Tree Shaking,因為它們對於任何型別的應用程式都是必不可少的。我們將這些必不可少的部分的度量標準稱為基礎尺寸。儘管增加了許多新功能,但Vue 3的基準尺寸gzip後大約只有10KB ,甚至還不到Vue 2的一半。

滿足規模需求

我們還想提高Vue處理大型應用程式的能力。我們最初的Vue設計著重於溫和的學習曲線。但是隨著Vue越來越廣泛地被採用,我們瞭解了更多有關專案需求的資訊,這些專案包含數百個模組,並且隨著時間的流逝由數十名開發人員維護。對於這些型別的專案,像TypeScript這樣的型別系統以及可複用程式碼的能力至關重要,而Vue 2在這些領域的支援並不理想。

在設計Vue 3的早期階段,我們嘗試通過提供對使用類編寫元件的內建支援來改善TypeScript整合。挑戰在於,我們需要使類可用的許多語言功能(如類欄位和裝飾器)仍是提案,並且在正式成為JavaScript一部分之前可能會發生變化。涉及到的複雜性和不確定性使我們懷疑新增Class API是否真的合理,因為它除了提供更好的TypeScript整合之外沒有提供任何其他功能。

我們決定研究其他解決擴充套件問題的方法。受到React Hooks的啟發,我們考慮過公開較低級別的資料驅動檢視和元件生命週期API,以實現一種更自由形式的編寫元件邏輯的方式,稱為Composition API。無需通過指定一長串選項來定義元件,Composition API允許使用者像編寫函式一樣自由地表達,編寫和重用有狀態元件邏輯,同時提供出色的TypeScript支援。

我們對這個想法感到非常興奮。儘管Composition API旨在解決特定類別的問題,但從技術上講,僅在編寫元件時才可以使用它。在該提案的初稿中,我們暗示我們可能會在將來的版本中將現有的Options API替換為Composition API。這導致社群成員的大量不滿,這為我們上了寶貴的一課,可以使他們清楚地傳達長期計劃和意圖,以及瞭解使用者的需求。在聽取了我們社群的反饋之後,我們對提案進行了完全的重新設計,從而明確表明Composition API將是對Options的補充和補充API。收到修改後的提案更加積極,並收到了許多建設性的建議。

尋求平衡

在Vue超過一百萬的開發人員中,有隻會HTML和CSS的基礎知識的初學者,有從jQuery遷移的老程式設計師,還有從另一個框架遷移的前端,甚至還有正在尋找前端解決方案的後端工程師、以及大規模處理軟體的軟體架構師。一些開發人員可能希望在舊版應用程式上增加互動性,而另一些開發人員可能需要敏捷開發但維護需求有限的一次性專案。在專案的整個生命週期中,可能不得不處理大型的,多年期的專案和一個波動的開發團隊。

在我們尋求平衡各種折衷方案的同時,Vue的設計不斷受到這些需求的影響和啟發。Vue號稱“漸進式框架”,封裝了由此過程產生的分層API設計。初學者可以使用CDN指令碼,基於HTML的模板和直觀的Options API來輕鬆學習,而高手可以使用功能齊全的CLI,渲染功能和Composition API來處理更復雜的專案。

要實現我們的願景,還有許多工作要做。最重要的是,更新周邊庫,文件和工具以確保順利遷移。在接下來的幾個月中,我們將繼續努力,我們迫不及待地想看看社群將通過Vue 3創造什麼。