尤雨溪:重頭來過的 Vue3 帶來了什麼?

作者:尤雨溪,翻譯:CSDN

英文原文:https://increment.com/frontend/making-Vue-3/

在過去的一年裏,Vue 團隊一直在開發 Vue.js 的下一個主要版本 Vue3,我們希望能在 2020 年上半年將其發佈(在撰寫本文時,這項開發工作正在進行中)。

重寫 Vue 新的主要版本的構想是在 2018 年底形成的,當時 Vue2 的代碼庫大約已有兩年半的運行歷史。這看起來不像是通用軟件生命週期中的一段很長的時間,但在這段時間裏,前端環境發生了巨大的變化。

兩個關鍵的因素導致了我們考慮重寫 Vue 新的主要版本:

  1. 主流瀏覽器對新的 JavaScript 語言特性的普遍支持。

  2. 當前 Vue 代碼庫隨着時間的推移而暴露出來的設計和體系架構問題。

1、爲什麼要重寫?

▐  使用新的語言特性

隨着 ES2015 標準的發佈,Javascript(正式稱爲ECMAScript,簡稱ES)得到了重大改進,主流瀏覽器終於開始爲這些新添加的特性提供適當的支持。其中一些特性特別地爲我們提供了極大提升 Vue 能力的機會。

其中一個最值得注意的特性是 Proxy,它允許框架攔截針對對象(屬性)的操作。Vue 的一個核心特性是能夠監聽對用戶定義的狀態所做的更改,並對 DOM 進行響應式地更新。Vue2 通過使用 getter 和 setter 來替換有狀態對象的屬性,來實現這種響應式的更新。切換到 Proxy 方式將允許我們消除 Vue 現有的限制(例如無法檢測新的屬性添加),並提供更好的性能。

然而,Proxy 是一個原生語言特性,在傳統瀏覽器中這個特性無法用 polyfill 來兼容。因此,爲了利用這個特性,我們必須調整Vue框架的瀏覽器支持範圍,這是一個重大的突破性的改變,只能在新的主要版本中發佈。

▐  解決體系架構存在的問題

在維護 Vue2 的過程中,我們積累了一些由於現有體系架構的限制而難以解決的問題。例如,模板編譯器的編寫方式使得正確的源映射(source-map)支持非常具有挑戰性。另外,雖然 Vue2 在技術上支持構建針對非 DOM 平臺的更高級別的渲染器,但爲了實現這一點,我們必須分叉代碼庫並複製大量代碼。在當前的代碼庫中修復這些體系架構問題將需要風險巨大的重構工作,而這些重構幾乎等同於重寫。

同時,我們還積累了技術債務,表現爲各種模塊的內部和似乎不屬於任何地方的浮動代碼之間的隱式耦合。這使得孤立地理解代碼庫的一部分變得更加困難,我們注意到貢獻者中很少有人有信心做出大範圍的更改。重寫將使我們有機會帶着這些問題和想法來重新考慮整個代碼庫結構。

2、初始原型的構建階段

我們在2018年底開始 Vue3 的原型開發,初步目標是驗證這些問題的解決方案。在這一階段,我們主要致力於爲進一步開發打下堅實的基礎。

▐  切換到 TypeScript

Vue2 最初是用純 ES(Javascript)寫成的。在原型設計階段之後不久,我們意識到一個類型系統(Type system)對於這樣一個規模的項目非常有用。類型檢查(Type check)大大減少了在重構過程中引入意外錯誤的機會,並幫助貢獻者更有信心進行大範圍的更改。我們採用了 Facebook 的Flow type checker,因爲它可以逐漸添加到現有的純 ES 項目中。Flow type checker 在一定程度上起到了幫助作用,但我們並沒有從中得到我們所希望的那麼多好處。特別是,持續的重大改變使得升級成爲一種痛苦。相比較 TypeScript 與 Visual Studio Code 集成開發工具的深度集成,Flow type checker 對集成開發環境的支持也不理想。

我們還注意到,用戶越來越多地同時使用 Vue 和 TypeScript。爲了支持它們的用例,我們必須獨立於使用不同類型系統的源代碼來編寫和維護 TypeScript 聲明。切換到 TypeScript 將允許我們自動生成聲明文件,從而減輕維護負擔。

▐  解耦內部包

我們還採用了monorepo設置,其中框架由內部包組成,每個包都有各自的API、類型定義和測試程序。我們希望能夠使這些模塊之間的依賴關係更加明確,以便開發人員能夠更容易地閱讀、理解和更改所有模塊。這是我們努力降低項目貢獻障礙和提高其長期可維護性的關鍵。

▐  設置 RFC(徵求修正意見)流程

到 2018 年底,我們已經成功構建了一個可工作的原型,它帶有新的響應式系統和虛擬 DOM 渲染器。我們已經驗證了我們想要進行的內部架構改進,但是面向公衆的 API 的更改還停留在草稿階段。是時候把它們變成具體的設計了。

我們知道我們必須儘早並且小心地處理這件事。Vue 的廣泛採用意味着任何重大改變都可能導致用戶的巨大遷移成本和潛在的生態系統碎片化。爲了確保用戶能夠提供關於這些重大改變的反饋,我們在 2019 年初採用了RFC(徵求修正意見)流程。每個 RFC 都使用一個模板,重點關注動機、設計細節、權衡和採用策略。由於該過程是在 GitHub repo 中進行的,我們將我們建議的更改作爲pull 請求提交,因此討論以評論的形式有效地展開了。

事實證明,RFC 流程非常有效,它作爲一個思想框架,迫使我們充分考慮潛在變化的方方面面,並允許我們的社區參與設計過程,提交深思熟慮的功能請求。

3、更快更小

性能對前端框架至關重要。儘管 Vue2 號稱具有良好的性能,但重寫提供了一個機會,可以通過試驗新的渲染策略來更提供更好的性能。

▐  克服虛擬 DOM 的瓶頸

Vue 有一個相當獨特的渲染策略:它提供類似於 HTML 的模板語法,但是,它是將模板編譯成渲染函數來返回虛擬 DOM 樹。Vue 框架通過遞歸遍歷兩個虛擬 DOM 樹,並比較每個節點上的每個屬性,來確定實際 DOM 的哪些部分需要更新。由於現代 JavaScript 引擎執行的高級優化,這種有點暴力的算法通常非常快速,但是 DOM 的更新仍然涉及許多不必要的 CPU 工作。當你看到一個基本上是靜態內容、只有少量動態綁定的模板時,效率低下的情況尤其明顯,因爲這時候仍然需要遞歸地遍歷整個虛擬 DOM 樹,以找出需要更改的內容。

幸運的是,模板編譯步驟使我們有機會對模板執行靜態分析並提取有關動態部分的信息。Vue2 在某種程度上是通過跳過靜態子樹來實現的,但是過於簡單的編譯器體系架構使得更高級的優化很難實現。在 Vue3 中,我們使用適當的 AST 轉換管道重寫編譯器,這允許我們以轉換插件的形式將編譯時(compile-time)優化組合進來。

隨着新的體系架構的出現,我們希望找到一種能夠儘可能減少開銷的渲染策略。一種選擇是拋棄虛擬DOM並直接生成命令式DOM操作,但這樣做會消除直接編寫虛擬DOM渲染函數的能力,而我們發現這種能力對於高級用戶和庫的編寫者非常有價值。另外,這將是一個巨大的突破性改變。

另一個更好的辦法是去掉不必要的虛擬 DOM 樹遍歷和屬性比較,這在更新期間往往會產生最大的性能開銷。爲了實現這一點,編譯器和運行時需要協同工作:編譯器分析模板並生成帶有優化提示的代碼,而運行時儘可能獲取提示並採用快速路徑。這裏有三個主要的優化:

1、在 DOM 樹級別。我們注意到,在沒有動態改變節點結構的模板指令(例如v-if和v-for)的情況下,節點結構保持完全靜態。如果我們將一個模板分成由這些結構指令分隔的嵌套“塊”,則每個塊中的節點結構將再次完全靜態。當我們更新塊中的節點時,我們不再需要遞歸遍歷 DOM 樹 - 該塊內的動態綁定可以在一個平面數組中跟蹤。這種優化通過將需要執行的樹遍歷量減少一個數量級來規避虛擬 DOM 的大部分開銷。

2、編譯器積極地檢測模板中的靜態節點、子樹甚至數據對象,並在生成的代碼中將它們提升到渲染函數之外。這樣可以避免在每次渲染時重新創建這些對象,從而大大提高內存使用率並減少垃圾回收的頻率。

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

綜合起來,這些技術大大改進了我們的渲染更新基準,Vue3 有時佔用的 CPU 時間不到 Vue2 的十分之一。

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

▐  最小化 bundle 的大小

框架的大小也會影響其性能。這是 Web 應用程序的一個獨特關注點,因爲所有相關的代碼需要動態下載,並且在瀏覽器解析出必要的 JavaScript 之前,應用程序不會進行交互。對於單頁應用程序尤爲如此。雖然Vue一直都比較輕量級, 但 Vue2 的運行時壓縮後的大小也約有 23KB,我們注意到兩個問題:

首先,並不是每個人都使用框架的所有特性。例如,一個從不使用 transition 特性的應用程序仍然需要付出與使用 transition 特性相關的代碼的下載和解析成本。

第二,隨着我們添加新特性,框架會無限制地增長。當我們權衡一個新特性添加的利弊時,我們就給了與 bundle 大小不成比例的權重。因此,我們傾向於只包含大多數用戶使用的特性。

理想情況下,用戶應該能夠在構建時刪除未使用的框架特性(也稱爲“樹抖動-tree shaking”)的代碼,並且只爲他們使用的代碼付出成本。這也將使我們能夠在不增加其他用戶的有效負荷成本的情況下,發佈一些用戶認爲有用的特性。

在 Vue3 中,我們通過將大多數全局 API 和內部幫助程序移動到 Javascript 的module.exports 屬性上實現這一點。這允許現代模式下的 module bundler 能夠靜態地分析模塊依賴關係,並刪除與未使用的 module.exports 屬性相關的代碼。模板編譯器還生成了對樹抖動友好的代碼,只有在模板中實際使用某個特性時,該代碼才導入該特性的幫助程序。

框架中有些部分永遠不會被“樹抖動”(這部分的代碼永遠不會從框架中刪除),因爲它們對任何類型的應用程序都是必不可少的。我們稱這些不可缺少的部分爲基線大小。儘管增加了許多新特性,但 Vue3 被壓縮後的基線大小約爲 10KB,不到 Vue2 的一半。

4、解決規模性的需求

我們還希望提高 Vue 處理大規模應用程序的能力。我們最初的 Vue 設計專注於低門檻和溫和的學習曲線。但是隨着 Vue 被越來越廣泛地採用,我們對包含數百個模塊並由數十名開發人員長期維護的項目的需求瞭解得更多。對於這種類型的項目,TypeScript 之類的類型系統和乾淨地組織可重用代碼的能力是至關重要的,而 Vue2 在這些領域的支持並不理想。

在設計 Vue3 的早期階段,我們試圖通過提供對使用類編寫組件的內置支持來改進 TypeScript 集成。然而挑戰在於,在正式成爲 JavaScript 的一部分之前,我們需要使類可用的許多語言特性(如類字段和裝飾器)仍然是建議的,並且可能會發生更改。所以,這種方法所涉及的複雜性和不確定性使我們懷疑添加類API 是否真的是合理的,因爲除了提供稍好一點的 TypeScript 集成之外,它沒有提供任何其他特性。

我們決定研究其他方法來解決規模性難題。受 React 鉤子的啓發,我們考慮公開較低級別的響應性和組件生命週期 API,以實現一種更自由形式的編寫組件邏輯的方法,我們稱之爲 Composition API。Composition API 不需要通過指定一長串選項來定義組件,而是允許用戶像編寫函數一樣自由地表達、組合和重用有狀態的組件邏輯,同時提供出色的 TypeScript 支持。

我們對這個主意很興奮。儘管 Composition API 是爲解決一類特定的問題而設計的,但在技術上只有在編寫組件時纔有可能使用它。在提案的初稿中,我們有點超前,並暗示在將來的版本中,我們可能會用 Composition API 替換現有的Options API,這導致了社區成員的強烈抵制。這件事給了我們一個寶貴的教訓,讓我們學會清楚地傳達長期計劃和意圖,以及理解用戶的需求。在聽取了我們社區的反饋後,我們徹底修改這個提議,明確表示 Composition API 將是Options API 的附加和補充。對於這一訂正提案的反饋要積極得多,並收到了許多建設性的建議。

5、尋求平衡

在 Vue 超過 100 萬開發人員的用戶羣中,有隻掌握 HTML/CSS 基礎知識的初學者,有從 jQuery 轉移來的專業人員,有從另一個框架遷移過來的老手,有尋找前端解決方案的後端工程師,以及處理大規模軟件的軟件架構師們。開發人員知識背景的多樣性導致了用例的多樣性:一些開發人員可能希望在老舊的應用程序上添加交互性,另一些開發人員可能正在處理一次性的項目,這些項目的週轉速度很快,但維護問題有限;而架構師們可能不得不在項目的生命週期中處理大型的、多年的項目和麪對不斷變化的開發團隊。

Vue 的設計不斷地受到這些需求的影響,我們試圖在各種需求之間取得平衡。Vue 的口號“漸進式框架”就是對通過這種過程產生的分層 API 設計的一種概括。初學者們可以通過使用 CDN 腳本、基於 HTML 的模板和直觀的 Options API,享受平滑的學習曲線,而專家們則可以使用功能齊全的 CLI、渲染函數和Composition API 來處理複雜的用例。

爲了實現我們的願景,還有很多工作要做。最重要的工作是更新支持庫、提供文檔和工具,以確保遷移的順利進行。我們將在接下來的幾個月裏努力工作,我們迫不及待地想看看Vue社區會使用Vue 3構建出什麼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章