【🚨萬字警告】了不得的Vue3(上)

寫在開頭

你們好,我是Channing

上週五剛在部門裏作完了關於Vue3的技術分享,先後大概花了整整一個星期的時間和精力去準備裏面的內容,遂想在週末的時間裏把這些從新再整理成文字也分享給掘友們。

或許有些部分不絕對專業,但絕對樂於接受合理的意見和建議。

本文主體脈絡分爲三個部分:Vue3重寫的動機優化的原理,以及Vue3帶來了什麼 值得一看的新東西

內容方面具體的劃分爲以下腦圖所示: javascript

Why——重寫的動機?

更專業可見尤大親筆:The process: Making Vue 3前端

重寫的動機主要分爲兩點:vue

  1. 使用新的JS原生特性
  2. 解決設計和體系架構的缺陷

使用新的JS原生特性

隨着前端標準化的發展,目前主流瀏覽器對不少JS新特性都廣泛支持了,其中一些新特性不只解決了不少技術上的實現難題,還帶來了更好的性能提高。java

在Vue3中,最重要也更爲人所知的就是ES6的Proxynode

Proxy不只消除了Vue2中現有的限制(好比對象新屬性的增長、數組元素的直接修改不會觸發響應式機制,這也是不少新手覺得所謂的bug),並且有着更好的性能:react

咱們知道,在Vue2中對數據的偵聽劫持是在組件初始化時去遍歷遞歸一個對象,給其中的每個屬性用Object.defineProperty設置它的getter和setter,而後再把處理好的這些對象掛到組件實例的this上面,因此這種方式的數據偵聽是在屬性層面的,這也是爲何增長對象屬性沒法被監聽的緣由,同時這種初始化的操做對於CPU來講仍是比較昂貴的一個操做。對於javascript而言,一個對象確定越穩定越小性能就越好。git

使用Proxy以後組件的初始化就不須要這麼作這麼費時的操做了,由於Proxy就是真正意義給一個對象包上一層代理從而去完成數據偵聽劫持的操做,因此總的來講這個複雜度是直接少了一個數量級的。只要你對這個代理後的對象訪問或修改裏面的東西都能被這層代理所感知到,進而去動態地決定返回什麼東西給你,而且也再也不須要把選項的這些東西再重複掛到組件實例的this上面,由於你訪問的時候是有這個信息知道你訪問的東西是屬於props仍是data仍是其餘的,vue只須要根據這個信息去對應的數據結構裏面拿出來就能夠了,單就這一點而言你就能夠感受到組件的內存佔用已經少了一半。github

因爲proxy是在對象層面上的代理,因此你在對象上新增屬性是能夠被代理到的。數組

另外Proxy還能夠代理數組,因此就算你直接修改數組裏面的元素也是能夠被代理到的。瀏覽器

可是,對於傳統的瀏覽器——IE,就連IE11也尚未支持Proxy這個東西,又因爲Proxy是原生的ES6特性,因此目前還沒法經過polyfill來兼容IE(Vue3也正在作這一塊的兼容).....這個東西也確實拿他沒轍,不然當初React升級到1五、Vue2.X也不會拋棄IE8了。尤大在去年的VueConf上還很形象地吐槽了IE——百足之蟲,死而不僵。

IE耗子尾汁吧。

解決設計和體系架構的缺陷

隨着Vue2使用和維護過程,逐漸暴露出來了一些設計和體系架構上的缺陷,主要有:

  1. 框架內部模塊的耦合問題
  2. TypeScript的支持很差
  3. 對於大規模應用的開發體驗很差

那麼在Vue3中是如何逐一解決這些問題的?

1. 解耦內部包

首先,看過Vue2源碼的朋友們應該比較深有感觸,單一地理解框架源碼是很是痛苦的。

這個表現爲各個模塊內部的高度耦合和看上去彷佛不屬於任何地方的浮動代碼的隱式耦合,這也讓源碼的維護和擴展在社區中變得困難重重。

也因爲內部模塊的耦合,對於一些資深的高級用戶(好比庫做者)在構建更高級別的渲染器時不得不把整個框架的代碼引入進來。咱們在看Vue2源碼的時候或許會注意到裏面還有Weex,這個是因爲Weex是與Vue官方合做的一個多端渲染框架,而Vue2中爲了支持這個能力又受限於現有架構,不得不分叉代碼庫而且複製大量的框架代碼進去,更慘的像mpVue這種非官方合做的,就只能手動拉整個Vue的分支代碼下來。

爲了解決這個問題,Vue3重寫時採用了monorepo的設置,把原來的各個模塊拆分出來,整個框架再由這些低耦合的內部包組成,每一個包都有本身的API、類型定義、測試程序等等。一方面讓開發人員能夠更容易地閱讀、理解甚至能夠放心地大範圍修改這些模塊包。

另外一方面還給予了用戶將其中的一些包單獨拿出去用的能力,好比你能夠把reactivity這個包也就是響應式系統拿出去用於須要用到響應式的場景,也能夠用這個包去搭一個本身的玩具框架等等都是能夠的。

2.使用TypeScript重寫以及設計類型友好的新API

講到TypeScript,這應該也是咱們比較關注的一個問題。

首先Vue2最初是使用純JS寫成的,但後來意識到一個類型系統將對這樣大型規模的項目是很是必要的,尤爲體如今重構或者擴展,類型檢查將很大程度地減小這個過程當中引入意外錯誤的機會,也讓更多代碼貢獻者能夠更大膽放心地進行大範圍的更改。

因此Vue2當時引入了Facebook的Flow進行靜態類型檢查,一方面是由於它能夠漸進地添加到現有的純JS項目中,但惋惜的是Flow雖然有必定的幫助可是並無指望中那麼香,最離譜的是誰能想到連Flow本身也都爛尾了,能夠上Flow的官網看看,這玩意到如今仍是0.X的版本,相比TypeScript的飛速發展以及TS與開發工具的深度集成尤爲是VSCode,Flow真的是一言難盡好吧。不能否認,尤大本身也說本身當初是壓錯寶了。

也因爲TS的蓬勃發展,愈來愈多的用戶也在Vue中使用TS,而對Vue來講,同時支持兩個類型系統是一件比較麻煩的事情,而且在類型推導上變得很是困難。若是源代碼切換到TS也就沒那麼多屁事了。

其次,之因此Vue2對TS的支持一塌糊塗,也是由於Options API與類型系統之間是存在斷層的。

Vue的API設計開始並無針對語言自己的機制和類型系統去設計,部分緣由也是Vue開始寫的時候js中甚至尚未類型系統這個玩意。

vue組件實例本質上就是個包含了一個個描述組件選項屬性的對象,這種設計的好處就是更符合人類的直覺,因此這也是爲何它對於新手來講更好理解和容易上手。

可是這種設計的缺陷就是跟TypeScript這樣的類型系統存在一個「斷層」,這個斷層怎麼說呢,對於不用類型系統只關注業務邏輯的用戶來講是感知不到的。

Vue2中的optionsAPI是一個看似面向對象可是實際上卻有必定誤差,這就致使了它不夠類型友好,尤爲是對於選項來講,類型推導是比較困難的。

但這個斷層其實也是雙向的:你能夠說是optionsAPI的設計不夠類型友好,也能夠說TS還不夠強大不能給Vue提供足夠好的類型支持。

舉個栗子,正如JSX一開始也是沒有類型支持的,徹底是由於TypeScript強行給加了一整套針對JSX的類型推導機制纔給了TSX如今的開發體驗。因此能夠這麼理解,若是TypeScript當時由於JSX不屬於真正的JS規範而不給它提供支持,是否是也能夠說React的設計跟類型系統存在着斷層呢?

那麼如何在Vue中去抹平這個斷層呢?一個很直接的方法就是從新設計一個類型友好的API。這個方法提及來很簡單,可是對Vue來講改一個API是須要考慮不少東西的:

  1. 與原有API的兼容性:可否同時支持新舊API?舊的用戶又如何升級?像Angular2當時那樣直接改的面目全非固然比較簡單,但說直接點就是無論舊版本用戶的死活,下場你們也清楚。如今主流的框架大版本升級都開始在版本兼容上足夠重視或者下了大功夫,好比十月份正式發佈的React17,這個版本沒有任何新的用戶層面的API,但其中一個有意思的新特性就是讓一個React應用能夠同時加載多個React版本,使得舊版本能夠逐步升級。

  2. 如何設計出既能提供良好的類型推導,又不讓類型推導而作的設計影響到非TS用戶的開發體驗?如何在TS和非TS的使用體驗中作到一個最好的平衡一致性?像Angular那樣無論非TS的用戶固然也是比較簡單的,可是Vue不會這麼作。

咱們回顧一下Vue2裏面是怎麼去使用TypeScript的:

在Vue2中使用過TypeScript的話咱們基本對這兩個社區方案比較熟悉了——vue-class-componentvue-property-decorator

這兩個方案都是基於Class實現的,那麼Vue3要作到類型友好,既然有了這麼成熟的兩個社區方案,在Vue3中繼續沿用這個方向,基於Class設計出一個更好用的API不就簡單完事了嗎?

確實,在Vue3的原型階段甚至已經實現了新的Class API,可是後面又把這個API給刪了。由於class的水真的是太深了。

首先,Class API依賴於fields、decorators等提案,尤爲是decorators的提案真的是太多坑了,咱們能夠看看github上TC39關於decorators提案的討論和進度:github.com/tc39/propos…

這玩意目前仍有41個在討論中的issue,在已關閉的167個issue中比較有意思的是以前V8團隊出於性能考慮直接否決掉了decorators其中的一個提案,有個老哥在底下評論說球球不要由於這個提案也推翻以前的提案,由於社區中已經有不少人在使用了,好比EmberAngularVue等等。

而decorators自己經歷了這麼長時間的爭論,已經大改了好幾回,但也仍然停留在stage2的階段:

stage2是什麼概念?能夠在TC39在About中貼出來的文檔中看到:

stage2意味着這個提案的東西隨時可能會發生翻天覆地的變化,至少得進入stage3階段纔不會出現破壞性的改動。

那麼如今TS裏面的decorators還能用是由於TS實現的是decorators比較早期的一個版本,已經跟最新的decorators提案脫節了,期間decorators還通過了幾回的大改。

另外,VueLoader裏面用的Babel對decorators的實現和TS對decorators的實現又有不一樣,這在一些比較極端的用例裏面可能就會踩坑了。

因此出於Class的複雜性不肯定性,這玩意在Vue3仍是暫時不考慮了,而且Class API除了稍微好一點的類型支持之外也並無帶來其餘的實用性。可是爲了版本兼容,Vue3中也仍然會支持剛剛提到的兩個社區方案。那麼拋棄了Class API,要怎麼去擁抱TypeScript呢?

事實上Class的本質就是一個函數,因此一個基於function的API一樣能夠作到類型友好,而且能夠作得更好,尤爲是函數中的參數和返回值都是對類型系統很是友好的,所以這個基於函數的API就應運而生了,也就是如今Vue3中的Composition API

3. 解決開發大規模應用的需求

隨着Vue被愈來愈普遍地採用,開發大型項目的需求也愈來愈多,對於這種類型的項目,首先須要的是一個像TypeScript這樣的類型系統,還須要能夠乾淨地組織可重用代碼的能力

巧妙的是,基於函數的Composition API,也叫作組合API,把這些需求全都給解決了,好傢伙!對於Composition API我會在第三部分中再去進一步談談。


How——如何優化?

關於優化,主要從兩個方面談談:如何更快如何更小

如何更快?

  1. Object.defineProperty => Proxy
  2. 突破Virtual DOM瓶頸
  3. 更多編譯時優化
  • Slot 默認編譯爲函數

  • 模板靜態分析生成VNode優化標記——patchFlag

Object.defineProperty => Proxy

這部分咱們剛剛已經講過了,它不只讓內存佔用變得更小,還讓組件的初始化變得更快,那麼有多快呢?

我搬運了Vue3原型階段Vue2.5的一個初始化性能測試對比圖,測試的benchmark是渲染3000個帶狀態的組件實例

能夠看到,內存佔用僅僅爲Vue2的一半,初始化的速度快了將近一倍

可是,還不夠!

這只是初始化,咱們看看組件更新時的優化。

突破Virtual DOM瓶頸

首先,咱們看看傳統的Virtual DOM 樹是如何更新的:

當數據發生改變的時候,兩棵vdom的樹會進行diff比較,找到須要更新的節點再patch爲實際的DOM更新到瀏覽器上。這個過程在Vue2中已經優化到了組件的粒度,經過渲染Watcher去準確找到須要更新的組件,將整個組件內的vdom tree進行diff。這個組件粒度的優化React也作到了,只不過這個優化的操做是交給了用戶,好比利用pureComponengshouldComponentUpdate等等。

但組件的粒度仍是相對比較粗的,因而Vue3重寫了Virtual Dom,以利用模板的靜態分析優點去將更新的粒度進一步縮小到動態元素甚至是動態的屬性

咱們先看一個最簡單的狀況:

在傳統的Virtual DOM下的diff過程:

咱們能夠看到,在這個模板下,整個組件節點的結構是固定不變的,而裏面有夾雜不少徹底靜態的節點,只有一個節點的文本內容是動態的。而在傳統的vdom下,仍然去遍歷diff了這些徹底不會發生變化的節點。雖然Vue2已經對這些徹底靜態的節點進行了優化標記以一種fastPath的方式去跳過這些靜態節點的diff,但仍然存在一個遍歷遞歸的過程。

那麼在Vue3新的Virtual DOM下,會如何進行diff呢?

經過compiler對模板的靜態分析,在優化模式下將靜態的內容進行hosting,也就是把靜態節點提高到外面去,實際生成vnode的就只有動態的元素<p class="text">{{ msg }}</p>,再分析這個元素內可能發生變化的東西,對這個元素打上patchFlag,表示這個元素可能發生變化的類型是文本內容textContent仍是屬性類class等等。

咱們看看模板編譯爲render函數後的結果:

能夠看到,徹底靜態的元素已經被提高到render函數上面去了,實際會建立vnode的就只有一個含有動態文本內容的p元素。

因此在新的Virtual DOM下,這個組件的diff過程就變成了:

肉眼可見的,這是一個數量級的優化。

那麼剛剛說了,這是一個組件節點結構徹底固定的狀況,那麼也就有另外一種狀況:動態節點

而在Vue的模板中,出現動態節點的狀況就只有兩種

  1. v-if
  2. v-for

先看v-if

咱們能夠看到,在v-if內部,節點結構又是徹底固定的,而且只有{{ msg }}是動態節點。因此若是把v-if劃分爲一個區塊Block的話,又變成了咱們上一個看的那種狀況。所以,只要先將整個模板看做一個Block,而後以動態指令進行劃分一個個嵌套的Block,每一個Block就都變成最簡單的那種狀況了:

而且每一個Block裏面的動態元素只須要以一個簡單的打平的數組去記錄跟蹤便可。因此diff的過程就只是遍歷遞歸去找那些存在動態節點的Block,根據這些動態Block中的一個數組就能夠完成diff的過程。

因此剛剛這個v-if的例子的新diff過程就是:

v-for也是相同的原理,將v-for劃分爲一個Block:

只有 v-for 是動態節點 ,每一個 v-for 循環內部:只有 {{ item.message }} 是動態節點。它的diff過程:

總結:

  • 將模版基於動態節點指令切割爲嵌套的區塊
  • 每一個區塊內部的節點結構是固定
  • 每一個區塊只須要以一個平面數組追蹤自身包含的動態節點

因此Virtual DOM的更新性能從與模板總體大小相關,提高到了只與動態內容的數量相關:

更多編譯時優化

  • Slot默認編譯爲函數

    這個讓使用插槽的父子組件之間的更新關係再也不強耦合

  • 利用模板靜態分析對vnode生成的類型標記——patchFlag

    這一點咱們剛剛也講到了,對於pacthFlag的定義,咱們能夠去源碼中看看(爲了方便截圖,我刪了部分的註釋,以及標註了前幾個的類型的二進制值出來):

<< 就是左移操做符,咱們能夠看到一共有十個動態的類型,每一個類型的數值都是在1的基礎上移動不一樣位數獲得的,因此一個十一位的二進制數就描述了vnode的動態類型。而且尤大很是友好地告訴咱們了這個怎麼用:

vnode的patchFlag經過 | 操做符去組合起來,vnode的patchFlag和某個特定類型所表明的patchFlag就用 & 操做符計算一下,若是獲得的結果爲0,則說明這個vnode的這個類型的屬性是不會變的,不爲0則相反。還引導了你去renderer.ts下看看怎麼使用的,不過他的路徑彷佛有點問題....我看的是packages/runtime-core/src/renderer.ts。但更深刻的內容就不在這裏展開了,感興趣的話之後能夠寫一篇專門講講這個吧。

看到尤大這個操做的時候真的是驚了,寫代碼還能這麼玩的啊?

而後靈光一閃,我尋思寫用戶鑑權好像也能夠這麼玩吧。

So,try it now:

還蠻有意思的~

言歸正傳,通過了這麼層層優化,Vue3究竟有多快

我去vue3.0 release時給出的數據docs.google.com/spreadsheet… 中搬運了過來:

能夠看到,與Vue2相比,Vue 3在bundle包大小減小41%、初始渲染快了55%、更新快了133% 和內存使用 減小54%


如何更小?

最主要的就是充分利用了Tree-shaking的特性,那麼什麼是Tree-shaking呢? 中文翻譯過來就是抖樹,咱們來看看它的工做原理

小玩笑...

MDN上對Tree shaking的描述:

什麼意思呢?爲了更好地體會到它的做用,咱們先看看兩種export的寫法:

第一種:

const msgA = 'hhhh'

const msgB = 777

const funcA = () => {
    console.log('AAA')
}

const funcB = () => {
    console.log('BBB')
}

export default{
    msgA,
    msgB,
    funcA,
    funcB
};
複製代碼

第二種:

export const msgA = 'hhhh'

export const msgB = 777

export const funcA = () => {
    console.log('AAA')
}

export const funcB = () => {
    console.log('BBB')
}
複製代碼

而後我在main.ts中分別引入並使用這兩個模塊:

第一種:

import TreeShaking1 from "@/benchmarks/TreeShaking1"

console.log(TreeShaking1.msgA)
TreeShaking1.funcA()
複製代碼

第二種:

import {funcA,msgA} from "@/benchmarks/TreeShaking2"

console.log(msgA)
// funcA()
複製代碼

build之後生成的app.js bundle

第一種:

第二種:

咱們能夠看到,tree shaking之後,進入bundle的只有被引入而且真正會被使用的代碼塊。在Vue3中許多漸進式的特性都使用了第二種的寫法來進行重寫,並且模板自己又是Tree shaking友好的。

但不是全部東西均可以被抖掉,有部分代碼是對任何類型的應用程序都不可或缺的,咱們把這些不可或缺的部分稱之爲基線大小,因此Vue3儘管增長了不少的新特性,可是被壓縮後的基線大小隻有10KB左右,甚至不到Vue2的一半

我把剛剛的兩個demo所在的項目build之後:

能夠看到這個app.js的bundle只有9.68kb,這仍是包括了router在內的,而以往Vue2構建出來的廣泛都在20+kb以上。


因爲篇幅緣由,剩下的內容將在下一篇爲你們分享。

【🚨萬字警告】了不得的Vue3(下)

下一篇中,讓咱們一塊兒去看看Vue3給咱們帶來了什麼值得一看的新東西(內含精彩Demo):

  • Composition API
  • Fragment
  • Suspense
  • TelePort
  • createRenderer API
  • Vite
相關文章
相關標籤/搜索