做者 | 尤雨溪
譯者 | 王強
編輯 | 蔡芳芳前端
尤雨溪在今年年初 Vue 3 正式發佈以前撰寫了這篇長文,詳述 Vue 3 的設計過程。前端之巔將全文翻譯以下,但願能幫助你更好地瞭解 Vue 3 背後的故事。vue
在過去的一年中,Vue 團隊一直都在開發 Vue.js 的下一個主要版本,咱們但願能在今年上半年發佈它(本文完成時這項工做尚在進行)。Vue 新版本的理念成型於 2018 年底,當時 Vue 2 的代碼庫已經有兩歲半了。比起通用軟件的生命週期來這好像也沒那麼久,但在這段時期,前端世界已經今昔非比了。git
在咱們更新(和重寫)Vue 的主要版本時,主要考慮兩點因素:首先是新的 JavaScript 語言特性在主流瀏覽器中的受支持水平;其次是當前代碼庫中隨時間推移而逐漸暴露出來的一些設計和架構問題。github
隨着 ES2015 標準的落地,JavaScript(之前被稱爲 ECMAScript,縮寫爲 ES)得到了諸多重大改進,同時主流瀏覽器也終於開始對這些新特性提供良好的支持了。其中的一些特性使咱們可以大幅提高 Vue 的能力。算法
這裏面最值得一提的就是 Proxy:後端
developer.mozilla.org/en-US/docs/…api
它爲框架提供了攔截針對對象的操做的能力。Vue 的一項核心特性就是監聽用戶定義狀態的變化,並響應式更新 DOM。Vue 2 是經過替換狀態對象屬性的 getter 和 setter 來實現這種響應能力的。轉向 Proxy 後,咱們就能解決 Vue 當下存在的諸多侷限(好比沒法檢測新增屬性等),還能提供更好的性能。數組
但 Proxy 是一個原生的語言特性,沒法在老式瀏覽器中提供完整的 polyfill。爲此咱們須要改動新版框架的瀏覽器支持範圍——這是一項破壞性變動,只有新的主要版本才能實現。瀏覽器
在現有代碼庫上修復這些問題須要大量高風險的重構工做,幾乎等同於重寫了。前端框架
在維護 Vue 2 的過程當中,咱們積累的不少問題受限於現有的架構是很難解決的。例如,模板編譯器的寫法使咱們很難實現良好的源映射支持。另外,雖然 Vue 2 技術上支持構建以非 DOM 平臺爲目標的高級渲染器,但爲了實現這一支持,咱們須要 fork 代碼庫,還得複製一大堆代碼。在現有代碼庫上修復這些問題須要大量高風險的重構工做,幾乎已經等同於重寫了。
同時,咱們在不少內部模塊與看起來無處可去的零散代碼之間生成了不少隱藏的耦合關係,結果積累了很多技術債。如今咱們很難單獨理解代碼庫中某一部分的含義,並且咱們也注意到貢獻者們不多有信心作出突破性的更改。經過重寫,咱們得以基於這些問題從新思考代碼的組織方式。
咱們是從 2018 年底開始建立 Vue 3 的原型的,主要目標是驗證針對上述問題的解決方案。在這一階段,咱們主要是爲後續的開發工做打下牢固的基礎。
Vue 2 最初是用純粹的 ES編寫的。原型階段開始後不久,咱們意識到對於這麼大規模的項目來講,類型系統會很是有用。類型檢查能夠大幅下降在重構中引入意外 bug 的概率,也能提高貢獻者在作出突破性更改時的信心。咱們採用了 Facebook 的 Flow 類型檢查器,由於它能夠漸進添加到一個現有的純 ES 項目中。Flow 起了必定做用,但咱們的收益不及預期;特別是它的重大更改太多了,升級起來至關痛苦。它對集成開發環境的支持也不如 TypeScript 與 VS Code 的深度集成水平。
咱們還注意到愈來愈多的用戶在結合使用 Vue 和 TypeScript。爲了支持他們的使用場景,咱們須要在源碼以外單獨編寫和維護一套 TypeScript 聲明,其使用了另外一套類型系統。轉向 TypeScript 後,咱們就能自動生成聲明文件,下降維護成本。
咱們還採用了一個單體倉庫方案,其中框架是由衆多內部包組成的,每一個包都有本身獨立的 API、類型定義和測試用例。咱們想讓各個模塊間的依賴關係更明顯,讓開發人員更容易閱讀、理解和修改全部這些依賴項。這是咱們下降項目貢獻門檻,提高其長期可維護性的關鍵舉措。
2018 年底,咱們有了一個帶有新的響應系統和虛擬 DOM 渲染器的原型。咱們驗證了計劃中的內部架構優化,但只是粗略起草了面向外部的 API 更改想法。如今該將這些想法轉變爲具體的設計了。
咱們知道這一步要在早期謹慎進行。Vue 的普遍流行意味着重大更改可能會給用戶帶來巨大的遷移成本,還可能讓生態碎片化。爲了讓用戶對重大更改提交反饋,咱們在 2019 年初制定了一套 RFC(徵求意見)流程。
全部 RFC 都有一個模板,包括動機、設計細節、權衡以及採用策略等內容。這套流程的實現形式,是在一個 Github 倉庫上將提案提交成拉取請求,這樣天然就能夠在評論中討論提案了。
結果代表這個 RFC 流程很是有用。做爲一個思惟框架,它強制咱們全面考慮一個潛在更改的全部層面,並讓整個社區能夠參與到設計過程當中,並提交通過充分思考的特性需求。
前端框架的性能相當重要。
前端框架的性能相當重要。儘管 Vue 2 已經提供了頗具競爭力的性能表現,但此次重寫讓咱們有機會試驗新的渲染策略來進一步提高性能。
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。
vue-composition-api-rfc.netlify.com/
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 框架的建立者和項目領導,也是一位獨立開源開發者。
原文連接
本做品系 轉載(閱讀原文)