尤雨溪自述:打造Vue 3背後的故事

尤雨溪在今年年初Vue 3正式發佈之前撰寫了這篇長文,詳述Vue 3的設計過程。前端之巔將全文翻譯如下,希望能幫助你更好地瞭解Vue 3背後的故事。

在過去的一年中,Vue團隊一直都在開發Vue.js的下一個主要版本,我們希望能在今年上半年發佈它(本文完成時這項工作尚在進行)。Vue新版本的理念成型於2018年末,當時Vue 2的代碼庫已經有兩歲半了。比起通用軟件的生命週期來這好像也沒那麼久,但在這段時期,前端世界已經今昔非比了。

在我們更新(和重寫)Vue的主要版本時,主要考慮兩點因素:首先是新的JavaScript語言特性在主流瀏覽器中的受支持水平;其次是當前代碼庫中隨時間推移而逐漸暴露出來的一些設計和架構問題。

爲什麼重寫

利用新的語言特性

隨着ES2015標準的落地,JavaScript(以前被稱爲ECMAScript,縮寫爲ES)獲得了諸多重大改進,同時主流瀏覽器也終於開始對這些新特性提供良好的支持了。其中的一些特性使我們能夠大幅提升Vue的能力。

這裏面最值得一提的就是Proxy,它爲框架提供了攔截針對對象的操作的能力。Vue的一項核心特性就是監聽用戶定義狀態的變化,並響應式更新DOM。Vue 2是通過替換狀態對象屬性的getter和setter來實現這種響應能力的。轉向Proxy後,我們就能解決Vue當下存在的諸多侷限(比如無法檢測新增屬性等),還能提供更好的性能。

但Proxy是一個原生的語言特性,無法在老式瀏覽器中提供完整的polyfill。爲此我們需要改動新版框架的瀏覽器支持範圍——這是一項破壞性變更,只有新的主要版本才能實現。

解決架構問題

在現有代碼庫上修復這些問題需要大量高風險的重構工作,幾乎等同於重寫了。

在維護Vue 2的過程中,我們積累的很多問題受限於現有的架構是很難解決的。例如,模板編譯器的寫法使我們很難實現良好的源映射支持。另外,雖然Vue 2技術上支持構建以非DOM平臺爲目標的高級渲染器,但爲了實現這一支持,我們需要fork代碼庫,還得複製一大堆代碼。在現有代碼庫上修復這些問題需要大量高風險的重構工作,幾乎已經等同於重寫了。

同時,我們在很多內部模塊與看起來無處可去的零散代碼之間生成了很多隱藏的耦合關係,結果積累了不少技術債。現在我們很難單獨理解代碼庫中某一部分的含義,而且我們也注意到貢獻者們很少有信心做出突破性的更改。通過重寫,我們得以基於這些問題重新思考代碼的組織方式。

早期的原型階段

我們是從2018年末開始創建Vue 3的原型的,主要目標是驗證針對上述問題的解決方案。在這一階段,我們主要是爲後續的開發工作打下牢固的基礎。

轉向TypeScript

Vue 2最初是用純粹的ES編寫的。原型階段開始後不久,我們意識到對於這麼大規模的項目來說,類型系統會非常有用。類型檢查可以大幅降低在重構中引入意外bug的機率,也能提升貢獻者在做出突破性更改時的信心。我們採用了Facebook的Flow類型檢查器,因爲它可以漸進添加到一個現有的純ES項目中。Flow起了一定作用,但我們的收益不及預期;特別是它的重大更改太多了,升級起來相當痛苦。它對集成開發環境的支持也不如TypeScript與VS Code的深度集成水平。

我們還注意到越來越多的用戶在結合使用Vue和TypeScript。爲了支持他們的使用場景,我們需要在源碼之外單獨編寫和維護一套TypeScript聲明,其使用了另一套類型系統。轉向TypeScript後,我們就能自動生成聲明文件,降低維護成本。

解耦內部包

我們還採用了一個單體倉庫方案,其中框架是由衆多內部包組成的,每個包都有自己獨立的API、類型定義和測試用例。我們想讓各個模塊間的依賴關係更明顯,讓開發人員更容易閱讀、理解和修改所有這些依賴項。這是我們降低項目貢獻門檻,提升其長期可維護性的關鍵舉措。

制定RFC流程

2018年末,我們有了一個帶有新的響應系統和虛擬DOM渲染器的原型。我們驗證了計劃中的內部架構優化,但只是粗略起草了面向外部的API更改想法。現在該將這些想法轉變爲具體的設計了。

我們知道這一步要在早期謹慎進行。Vue的廣泛流行意味着重大更改可能會給用戶帶來巨大的遷移成本,還可能讓生態碎片化。爲了讓用戶對重大更改提交反饋,我們在2019年初制定了一套RFC(徵求意見)流程。所有RFC都有一個模板,包括動機、設計細節、權衡以及採用策略等內容。這套流程的實現形式,是在一個Github倉庫上將提案提交成拉取請求,這樣自然就可以在評論中討論提案了。

結果表明這個RFC流程非常有用。作爲一個思維框架,它強制我們全面考慮一個潛在更改的所有層面,並讓整個社區可以參與到設計過程中,並提交經過充分思考的特性需求。

更快,更小

前端框架的性能至關重要。

前端框架的性能至關重要。儘管Vue 2已經提供了頗具競爭力的性能表現,但這次重寫讓我們有機會試驗新的渲染策略來進一步提升性能。

突破虛擬DOM的瓶頸

Vue有一套獨特的渲染策略:它提供了一個類HTML的模板語法,但將模板編譯成了一個返回虛擬DOM樹的渲染函數。框架會遞歸遍歷兩個虛擬DOM樹,對比每個節點的所有屬性來判斷該更新DOM的哪些部分。這種相對暴力的算法一般還是很快的,這要感謝現代JS引擎實現的那麼多高級優化措施。但是更新過程還是會涉及很多不必要的CPU工作。當你的模板存在大量靜態內容,卻只有少量動態綁定時,更新的效率就會顯得尤爲低下——還是要遞歸遍歷整個虛擬DOM樹,才能找出要更新的部分。

所幸模板編譯這一步讓我們可以對模板進行靜態分析,並提取動態部分的信息。Vue 2跳過了靜態子樹,在一定程度上做到了這一點;但是由於過度簡化的編譯器架構,更高級的優化就很難實現了。在Vue 3中我們重寫了編譯器,加入了一個合適的AST transform管道,讓我們能以transform插件的形式進行編譯時優化。

現在有了新的架構,我們想要找到一個儘可能減少額外開銷的渲染策略。一個選項是拋棄虛擬DOM並直接生成命令式DOM操作,但這會失去直接編寫虛擬DOM渲染函數的能力,我們發現這是對於高級用戶和庫作者們非常有價值的能力。此外,這也會是一個影響巨大的重大更改。

接下來的選項就是擺脫不必要的虛擬DOM樹遍歷和屬性對比,這也是更新過程中性能開銷最大的部分。爲此,編譯器和運行時需要協同工作:編譯器分析模板,生成帶有優化線索的代碼,而運行時獲取線索並選擇最快路徑。這裏有三大優化工作:

首先,在樹級別,我們注意到沒有動態調整節點結構的模板指令(如v-if和v-for)時,節點的結構完全保持靜態。如果我們將模板根據這些結構化指令拆分爲一些嵌套"塊",每一個塊中的節點結構也會保持靜態。當我們更新一個塊中的節點時,就不必再遞歸遍歷整個樹了——塊內的動態綁定可以在一個平面數組裏追蹤。這一優化極大減少了需要遍歷的樹的數量,規避了大部分虛擬DOM樹開銷。

其次,編譯器會激進檢測模板中的靜態節點、子樹甚至數據對象,並在生成的代碼中將它們提取到渲染函數之外。這就可以避免在每次渲染時重新創建這些對象,大幅減少了內存佔用,並減少了垃圾收集的頻率。

最後,在元素級別,編譯器會爲每一個有動態綁定的元素,根據其需要進行的更新類型生成一個優化標誌。比如說一個元素有一個動態的class綁定和一些靜態屬性,它會獲得一個標誌,表示這裏只需要進行class檢查。運行時會獲取這些標誌,然後選擇最快的路徑。

CPU時間:是指JavaScript運算所消耗的時間,不包括瀏覽器DOM操作所用的時間。

結合這些優化,我們的渲染更新速度獲得了顯著改進,在某些場景下Vue 3的CPU時間僅爲Vue 2的十分之一不到。

縮小包體積

框架的大小也會影響其性能。這是Web應用程序特有的現象,因爲資產需要在線下載,而應用需要等到瀏覽器解析完必要的JavaScript代碼後才能開始交互。單頁面應用程序在這方面的矛盾尤爲明顯。儘管Vue一直以來都是相對輕量級的框架——Vue 2的運行時大小爲23KB(gzip壓縮後),我們還是注意到了兩個問題:

首先,不是所有人都需要框架的全部功能。例如,從來不需要過渡特性的應用還是需要下載和解析相關代碼。

另外,我們在不斷給框架增加新特性,框架也在不斷變大,沒有止境。這樣我們在權衡新特性的利弊時,就得非常在意包大小這個權重。結果,我們會傾向於只加入那些大多數用戶都會用到的特性。

理想狀態下,用戶可以在構建時去掉框架中自己不需要的特性(也就是"搖樹優化"),只保留自己用到的特性。這樣我們在添加只有部分用戶會用到的特性時,並不會給其他用戶增添應用體積的負擔。

在Vue 3中,我們把大多數全局API和內部helper移到了ES模塊導出中,從而實現了這個目標。這樣現代的打包器就可以靜態分析模塊依賴項,並去掉與未使用導出相關的代碼。模板編譯器也會生成適合搖樹優化的代碼,只會對模板確實用到的特性導入helper。

框架的有些部分是永遠無法搖樹優化的,因爲它們對於所有應用類型來說都很重要。我們將這部分無法捨棄的代碼的體積稱作基線大小。雖然Vue 3增加了很多新特性,但其基線大小隻有大約10KB(gzip後),不到Vue 2的一半。

滿足擴展需求

我們還想改善Vue應對大規模應用程序的能力。我們最初設計Vue時主要想的是降低入門門檻並平滑學習曲線。但隨着Vue愈加流行,我們也看到了越來越多的項目需求隨着時間推移不斷擴大,後期甚至包含數以百計的模塊,需要幾十名開發人員來維護。對於這種類型的項目,TypeScript這樣的類型系統和可以提供組織清晰、易於複用的代碼的能力是必不可少的,但Vue 2在這些方面的支持水平不甚理想。

在Vue 3的早期設計階段,我們嘗試內置對使用class編寫組件的支持,從而更好地整合TypeScript。這裏的問題在於,爲了讓class可用而需要的很多語言特性(例如class fields和 decorators)都還處在提案階段,有可能在正式版中出現變化。隨之而來的複雜性和不確定性讓我們開始質疑Class API是否真的合適,因爲它只能改善一點TypeScript的整合能力而已。

於是我們決定探索其他途徑來解決擴展問題。受到React Hooks的啓發,我們想到了暴露底層的響應式和組件生命週期的API,從而提供一種更靈活地編寫組件邏輯的方式,也就是Composition API。Composition API不再需要用一個長長的配置列表定義組件,它允許用戶自由定義、組合和重用組件邏輯,就像寫函數一樣,同時還能提供完善的TypeScript支持。

我們非常喜歡這個想法。儘管Composition API是爲解決特定類型的問題設計的,但也能用在單純的組件開發中。在提案的初稿中我們有些忘乎所以,暗示我們可能會在未來的版本中用Composition API替換掉當前的Options API。這引起了社區成員的極大反彈,給我們上了重要的一課,讓我們認識到了與社區溝通長期計劃和發展方向,以及理解用戶需求的重要性。在聽取社區反饋之後,我們完全重做了提案,確認Composition API只是錦上添花,是Options API的補充。新版提案的反饋要正面許多,我們還收到了很多建設性的意見。

把握平衡

開發人員的多樣性意味着使用場景的多樣性。

如今有超過一百萬的開發人員在使用Vue,其中有隻懂一點HTML/CSS的新手,從jQuery一路走來的專家,從其他框架遷移過來的老鳥,在尋找前端解決方案的後端工程師,還有負責設計大規模軟件的架構師。開發人員的多樣性意味着使用場景的多樣性:有的開發人員可能想要提升舊項目的交互體驗,另一些人可能想要快速開發低成本的一次性項目;架構師可能要應對規模巨大的長期項目,以及項目生命週期內的開發團隊成員變動。

Vue的設計在不斷根據這些需求變化和發展,我們也設法從諸多權衡中找到平衡點。Vue的口號“漸進式框架”,背後就是這個過程中形成的分層API設計。新手可以通過CDN script、基於HTML的模板以及直觀的Options API順利學習入門。而專家可以通過全功能的CLI、渲染函數以及Composition API來處理複雜需求。

要實現我們的願景還有很多工作要做,其中最重要的就是更新支持庫、文檔和工具,以保證平滑的遷移。我們會在未來的幾個月中繼續努力,而且我們迫不及待想要看到社區能用Vue 3創造怎樣的精彩了。

作者介紹

尤雨溪是Vue.js框架的創建者和項目領導,也是一位獨立開源開發者。

原文鏈接:

https://increment.com/frontend/making-vue-3/

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