升级 Vue3 大幅提升开发运行效率
作者:louiszhai,騰訊 IEG 前端開發工程師
Vue3 性能提升了 1.3~2 倍,SSR 性能提升了 2~3 倍,升級 Vue3 正是當下。
背景
原計劃 2019 年發布的 Vue3,又經過一年的再次打磨,終于于去年 9 月正式發布。隨后,不少 UI 組件庫都積極參與適配,去年 12 月,Element-plus(Element-ui 官方升級版)也發布了 beta 版。
由于項目中用到了 Element-ui 組件,組件庫未適配的情況下,不敢貿然升級 Vue3。Element-plus 發布后,又經過 1 個月的觀察、測試和調研,發現 Element-plus 相對成熟(還有少量 bug,后續會講),便開始嘗試升級 Vue3。
如何升級 Vue3
有兩種方案可以快速升級 Vue3:
一種是使用微前端輪子,我基于 qiankun2,搭建了 Vue3 項目基座,為了保證平穩升級,子項目繼續使用 Vue2,然后不斷的把子項目的頁面遷移到基座項目。
另一種是,直接升級 Vue3,將項目中的 Vue2 依賴庫升級到 Vue3 的最新版(當前最新版是v3.0.11),并且稍微改造 webpack 編譯腳本,使之適配 Vue3。
之所以會有方案一,主要還是擔心 Element-plus 不夠穩定,如果有天坑,又無法繞過去,除了向餓了么團隊提交 PR,微前端兜個底也是不錯的應急措施。
就這樣微前端方案又運行了 1 個月,部分頁面已完成升級,運行良好,實踐證明 Element-plus 比想象中穩定,這增加了我對于方案二的信心。考慮到還有少量業務復雜的頁面,在微前端模式下,子項目的各種數據多經過一層 qiankun 的 proxy 代理,性能有損耗,影響了頁面更新,于是一次性將剩余的頁面全部遷移到 Vue3 項目中。
實踐證明,除非比較復雜的項目,或者依賴組件庫沒升級等原因不適合升級外,常規情況下,升級 Vue3 都是一個不錯的選擇。
為什么要升級 Vue3
為什么要升級 Vue3,這是一個幾乎不需要回答的問題。升級 Vue3 后,代碼結構更加清晰內聚,響應式數據流更加可控,節省了很多心智成本,從而使得開發效率大幅提升。Vue3 還帶來了很多新特性,框架層面運行性能更高(性能提升了 1.3 至 2 倍,SSR 性能提升了 2 至 3 倍),Composition API 使得代碼拆分,函數封裝更容易,復雜項目也隨之更容易管理。
Vue2 中,相關的邏輯經常分散在 option 的 data、watch、computed、created、mounted 等鉤子中,閱讀一段代碼,經常需要上下反復橫跳,帶來了部分閱讀障礙。鉤子又依賴 Vue 實例,代碼封裝基于天生攜帶鉤子的 Mixin 去做,更加容易和相對方便。
但正因為如此,Mixin 的鉤子容易不自覺的越界,插手到頁面或組件的內部變量和方法管理過程中;甚至,多個不同的 Mixin,相互之間就很容易沖突,項目開發者,在引入 Mixin 和避免沖突之間需要保持微妙的平衡,不但增加心智負擔,還帶來了副產品:本身撲朔迷離的 this 變得更加不確定。因此,大型項目 Mixin 幾乎都是一種反模式。
現在這些框架問題,都由 Vue3 的 Composition API 解決了。
Vue3 帶來了哪些新特性
我們先看一些立馬能感受到變化的特性。
Proxy 代理
這是一個一上手 Vue3 就能感知的變化。即使你在 Vue3 中編寫 Vue2 風格的基于 option 的代碼,Proxy 也是默默提供著數據響應式。
const?observe?=?(data)?=>?{Object.keys(data).forEach((key)?=>?{const?initValue?=?data[key];let?value?=?initValue;if?(typeof?initValue?===?'object')?{observe(initValue);return;}Object.defineProperty(data,?key,?{enumerable:?true,configurable:?true,get()?{console.log('visit?key?value?=',?key,?value);return?value;},set(val)?{console.log(`[${key}]changed,old?value=${value},?new?value?=?${val}`);if(value?!==?val)?{value?=?val;}}});}); }; const?data?=?{}; Array.from(new?Array(100),?()?=>?"").forEach((item,?i)?=>?{data[i]?=?{?value:?i?*?2?}; }); console.time(); observe(data); console.timeEnd();?//?default:?0.225ms data.a?=?{?b:?1?}; data.a.b?=?2;如上所示,Vue2 的數據響應式是通過 Object.defineProperty 實現,這是一個深度遍歷的過程,無論 data 中包含多少層數據,都需要全部遍歷一遍。深度遍歷,給對象的每個自身屬性添加 defineProperty,需要不小的性能開銷,同時后面新增到 this 中的屬性不提供響應式監聽,因此我們需要使用諸如this.$set這種方式去添加新屬性。
Proxy 就沒有這個問題,如下所示。
const?observe?=?(data)?=>?{return?new?Proxy(data,?{get(target,?key,?receiver)?{console.log('visit',?key);return?Reflect.get(target,?key,?receiver);},set(target,?key,?value,?receiver)?{console.log(`[${key}]changed,?value?=?${value}`);Reflect.set(target,?key,?typeof?value?===?'object'???observe(value)?:?value,?receiver);}}); }; let?data?=?{}; Array.from(new?Array(100),?()?=>?"").forEach((item,?i)?=>?{data[i]?=?{?value:?i?*?2?}; }); console.time(); const?proxy?=?observe(data); console.timeEnd();?//?default:?0.041ms proxy.a?=?{?b:?1?};?//?[a]changed,?value?=?[object?Object] proxy.a.b?=?2;?//?visit?a?\n?[b]changed,?value?=?2Proxy 不但使得 data 獲得了新屬性的響應性,整個響應式處理過程的效率還提升了數倍,由此帶來了 Vue3 的大部分性能提升。
Composition API
為了保持對 Vue2 的向下兼容,Vue3 中仍然支持純 Option 配置的書寫方式,這為升級提供了便利,平移 Vue2 的代碼,只需少量改動,便可正常運行。
同時考慮到上手難度,Vue3 的頂層代碼風格與 Vue2 保持一致,依然是 export 一個對象,對象包含了一系列的配置,其中便有 setup 入口函數。我們先來看一段代碼,然后逐個解讀。
import?{?defineComponent,?ref,?reactive,?toRefs,?watch,?watchEffect,?computed,?onMounted?}?from?"vue"; export?default?defineComponent({setup(props,?context)?{const?selectRef?=?ref(null)?//?作為下拉框的ref引用const?state?=?reactive({?//?響應式數據,類似于Vue2的thisnum:?0,});const?{?init?}?=?toRefs(props);watch(()?=>?state.num,?(newVal,?oldVal)?=>?{console.log(newVal,?oldVal);});watchEffect(()?=>?{console.log(state.num);});const?num2?=?computed(()?=>?state.num?+?1);onMounted(()?=>?{state.loaded?=?true;});return?{?selectRef,?state,?num2,?init,?context?};} });setup 作為入口函數,包含兩個參數,分別是響應式的 props 外部參數,以及 context 對象,context 包含 attrs、emit、expose、props、slots 五個參數,如下所示:
在 Vue3 的設計里,setup,以及從 vue 對象中解構出來的各種生命周期函數,執行優先級高于 Vue2 中的各種生命周期鉤子,因此
beforeCreate()?{console.log('beforeCreate'); }, created()?{console.log('create'); }, setup()?{console.log('setup'); },這段代碼的輸出依次是 setup、beforeCreate、created。
ref、reactive
setup 中,第一句const selectRef = ref(null);,這里定義的是一個響應式的數據,可傳遞給 template 或 render,用于下拉框組件或下拉框 dom 綁定引用。為什么使用 ref,不使用 reactive 呢?ref 和 reactive 都可以給數據添加響應性,ref 一般用于給 js 基本數據類型添加響應性(當然也支持非基本類型的 object),reactive 只能用于代理非基本數據類型。null 是基本數據類型,只能使用 ref,那既然如此,為什么不在所有情況都使用 ref 呢?我們來看一段代碼:
const?num?=?ref(0); num.value?=?1; const?obj?=?{?a:?1?}; const?refObj?=?ref(obj); const?reactiveObj?=?reactive(obj); refObj.value.a?=?2; reactiveObj.a?=?3; console.log(num,?refObj,?reactiveObj);我們注意到,使用 ref api 時,數據變成了對象,值就是 value 屬性的值,如果數據本身就是對象,依然會多一層 value 結構,而 reactive 沒有這些副作用。同時,還有一個有意思的現象是,所有的源數據,都需要經過響應式 api 包裹,然后才能使用,這跟前面提到的 Proxy 原理有關,Proxy 代理數據時,需要基于返回的代理進行數據更新。
toRefs
除了 ref、reactive 外,還有一個常用的響應式 api——toRefs。為什么需要它,這是因為響應式對象,經過解構出來的屬性不再具有響應性,toRefs 就是為了快速獲得響應性的屬性,因此這段代碼const { init } = toRefs(props);,就是為了獲得響應式屬性 init,想要保留 props 參數的響應性,建議這么做。
watch、watchEffect
const?num?=?ref(0); const?state?=?reactive({num:?0, }); const?obj?=?{?num:?0?}; watch(num,?(newVal,?oldVal)?=>?{console.log("num",?newVal,?oldVal); }); watch(()?=>?state.num,?(newVal,?oldVal)?=>?{console.log("num",?newVal,?oldVal); }); watch(()?=>?obj.num,?()?=>?{console.log("這里不會執行"); }); num++; state.num++; obj.num++;如上,watch api,它需要接受一個具有返回值的 getter 函數或者 ref(如() => state.num,ref)。
如果需要監聽多個值,如下所示:
const?num1?=?ref(0); const?num2?=?ref(0); watch([num1,?num2],?([newNum1,?newNum2],?[prevNum1,?prevNum2])?=>?{console.log([newNum1,?newNum2],?[prevNum1,?prevNum2]); }); num1.value?=?1;?//?[1,?0],?[0,?0] num2.value?=?2;?//?[1,?2],?[1,?0]可見多個數據的每次更新都會觸發 watch。想要監聽一個嵌套的對象,跟 Vue2 一樣,依舊需要使用 deep 選項,如下所示:
const?state?=?reactive({attr:?{id:?1,}, }); watch(()?=>?state,?(currState,?prevState)?=>?{console.log(currState.attr.id,?prevState.attr.id,?currState?===?prevState,?currState?===?state);?//?2,?2,?true,?true },?{?deep:?true?}); watch(()?=>?state.attr.id,?(currId,?prevId)?=>?{console.log(currId,?prevId);?//?2,?1 }); state.attr.id?=?2;看到差別了嗎?監聽響應式對象時,返回的是對象的引用,因此 currState,prevState 指向是同一個最新的 state,如果需要獲取變化前的值,建議返回監聽的屬性,如watch(() => state.attr.id),剛好 state.attr.id 是一個基本類型的值,那么 deep 也不需要。
watchEffect 是 Vue3 新增的 api,watchEffect 會自動運行一次,用于自動收集依賴,但不支持獲取變化前的值,除此之外,與 watch 用法一致。那么 watchEffect 適用什么場景呢?這也是我剛上手 Vue3 的困惑之一。我們來看一段代碼:
const?rights?=?{admin:?["read",?"write"],user:?["read"], }; const?state?=?reactive({rights:?"", }) const?userInfo?=?reactive({?role:?"user"?}); userInfo.name?=?"Tom"; userInfo.role?=?"admin"; watch(()?=>?userInfo.role,?(newVal,?oldVal)?=>?{state.rights?=?rights[newVal]; }); watchEffect(()?=>?{state.rights?=?rights[userInfo.role]; });以上代碼中,watch 中的邏輯只能在 userInfo 變化后執行,因此 state.rights 不會提供初始值,相反,watchEffect 中 state.rights 由于自動依賴收集,獲得了一次賦值的機會。
這樣做的好處是什么呢?在實際項目中,userInfo.role 可能是一個全局 store 中的數據,用戶登錄進來后,就會通過接口獲取初始值,我們并不能確認,用戶進到其中一個頁面時,userInfo.role 的值是否已經被接口更新,且 userInfo 變化前的值我們也不關心,watchEffect 就非常適合這種場景,它會自動進行一次初始化,并且在變化后,及時更新值。
watch 和 watchEffect 的監聽會在組件銷毀時自動取消,除此之外,可以通過它們返回的函數手動取消監聽,如下所示:
const?stopWatch?=?watch(selectRef,?(newVal,?oldVal){}); const?stopWatchEffect?=?watchEffect(selectRef,?(newVal,?oldVal){}); setTimeout(stopWatch,?1000); setTimeout(stopWatchEffect,?1000);watchEffect 更多的用法,請參考官方文檔。
computed
computed 的使用如下:
const?num?=?ref(1); const?num2?=?computed(()?=>?num?*?2); num2.value++;?//?errornum2 是一個不可變的 ref 對象,不能直接對它的 value 屬性賦值。
computed 還可以接收一個帶有 get 和 set 函數的對象,來創建一個可讀寫的 ref 對象,如下所示:
const?num3?=?computed({get:?()?=>?num.value?*?2,set:?(val)?=>?{num.value?=?val;}, }); num3.value?=?100; console.log(num.value,?num3.value);?//?100?200自定義 Hooks
Vue3 的 Composition 之所以這樣實現,主要原因就是為了便于代碼拆分,降低耦合,我們不妨來實現一個自定義的 hooks。
//?page.vue import?useCount?from?"./useCount"; export?default?{setup()?{const?{?num,?double,?plus?}?=?useCount(1);return?{?num,?double,?plus?};}, }; //?useCount.js import?{?ref,?computed?}?from?"vue"; export?default?(value)?=>?{const?num?=?ref(value);const?double?=?computed(()?=>?num.value?*?2);const?plus?=?(val)?=>?num.value?+?val;return?{?num,?double,?plus?}; };useCount.js 就是一個自定義的 hooks,得益于 Vue3 的全局 API,我們可以輕松做到代碼拆分。Vue3 的 setup 聚合了所有的邏輯,容易產生面條代碼,合理使用自定義 hooks,可以有效的減少面條代碼,提升代碼可維護性。并且 Vue3 的 hooks 比 react 更加簡單高效,不會多次執行,不受調用順序影響,不存在閉包陷阱等等,幾乎可以沒有任何心智負擔的使用。
新的生命周期鉤子
看到這里,相信你對 Vue3 的生命周期已經有一些了解了,我們不妨來做個梳理。
Vue3 幾乎內置了所有的 Vue2 生命周期鉤子,也就是說,剛開始升級項目至 Vue3 時,可以直接使用 Vue2 的鉤子,方便平滑升級,如上圖左下角所示,有兩個鉤子發生了替換,beforeDestory 被替換成了 beforeUnmount,destoryed 被替換成了 unmounted。完整的鉤子對比如下:
除了 setup 外,Vue3 的其他生命周期鉤子都添加了 on 前綴,更加規范統一。新的鉤子需要在 setup 中使用,如下所示:
import?{?onMounted?}?from?"vue"; export?default?{setup()?{onMounted(()?=>?{console.log("onMounted");});}, };Tree-Shaking
Vue3 一共開放了 113 個 API,我們可以通過如下方式引用:
import?{?ref,?reactive,?h,?onMounted?}?from?"vue";通過 ES6 modules 的引入方式,能夠被 AST 靜態語法分析感知,從而可以只提取用到的代碼片段,最終達到 Tree-Shaking 的效果,這樣就使得 Vue3 最終打包出來的包更小,加載更快。據尤大去年 4 月在 B 站的直播:基本的 hello world 項目大小為 13.5kb,Composition API 僅有 11.75kb,包含所有的運行態僅 22.5kb。
Fragment
Vue3 中,Fragment 的引入,解決了組件需要被一個唯一根節點包裹的難題,帶來的是 dom 層級的減少,以及渲染性能的提升,某些時候,如下所示:
<!--?child.vue?--> <template><td>{{?title?}}</td><td>{{?subtitle?}}</td><!--?Vue2中template出現了多個根節點,無法編譯通過?--> </template> <!--?parent.vue?--> <template><table><tr><child?/></tr></table> </template>在 Vue2 中,這意味著我們沒辦法在 child.vue 的 template 中加入多個 td 節點,多個 td 可以被 tr 包裹,如果 child.vue 根節點替換為 tr,那么就會跟 parent.vue 的 tr 沖突。
同樣的代碼,在 Vue3 中就能正確編譯通過,這是因為 Vue3 中,組件的 template 被一層不可見的 Fragment 包裹,組件天生支持多個根節點的布局。
Teleport
Teleport 是 Vue3 新增的組件,即傳送門,Teleport 能夠在不改變組件內部元素父子關系的情況下,將子元素”傳送“到其他節點下加載,如下所示:
<template><div?class="container"?style="width:?100px;?height:?100px;?overflow:?hidden"><div?class="dialog"?style="width:?500px;?height:?400px;">...</div></div> </template>dialog 直接掛載在 container 下,超出部分將不可見。加一層 Teleport,我們可以輕松將 dialog 展示出來。
<template><div?class="container"?style="width:?100px;?height:?100px;?overflow:?hidden"><teleport?to="body"><div?class="dialog"?style="width:?500px;?height:?400px;">...</div></teleport></div> </template>dialog 依然處于 container 內部,僅僅只是被掛載到 body 上,邏輯關系不變,展示也不會遮擋。
Suspense
Vue2 中,我們經常寫這樣的 loading 效果,如下所示:
<template><div?class="container"><div?v-if="init"><list?/></div><div?v-else>loading~~</div></div> </template>Vue3 中,我們可以通過 Suspense 的兩個插槽實現以上功能,如下所示:
<template><div class="container"><Suspense><template #default><list /></template><template #fallback>loading~</template></Suspense></div> </template> <script>import { defineAsyncComponent } "vue";export default {components: {list: defineAsyncComponent(() => import("@/components/list.vue")),},}; </script>Vue3 知識圖譜
Vue3 還包括了一些其他常用更新,限于篇幅,這里先列出來,下篇再講。
實際上,Vue3 帶來的更新,遠不止這些,為此我梳理了一個 Vue3 的知識圖譜,盡可能囊括一些本文未提到的特性。
如上圖,Vue 不但重寫了 diff 算法,還在編譯階段做了很多優化,編譯時優化可以通過這個網站看出來:https://vue-next-template-explorer.netlify.app/。
Vue3 的開放生態
根據 Monterail 2 月份發布的第三版 Vue 生態報告,Vue 的流行度逐年上升,很多非 web 的可視化領域也可以基于 Vue 開發,特別是 Vue3 的渲染 API 的開放,使得基于 Vue 構建 Canvas、WebGL、小程序等應用更加方便,如下圖所示,60 行代碼實現一個簡單的 Canvas 柱狀圖:
import?{?createRenderer,?h?}?from?"vue"; const?renderer?=?createRenderer({createElement:?(tag)?=>?({?tag?}),patchProp:?(el,?key,?prev,?next)?=>?{?el[key]?=?next;?},insert:?(child,?parent)?=>?{?parent.nodeType?===?1?&&?draw(child)?}, }); let?canvas let?ctx; const?draw?=?(el,?noClear)?=>?{if?(!noClear)?{ctx.clearRect(0,?0,?canvas.width,?canvas.height);}//?柱狀圖繪制邏輯if?(el.tag?==?'chart')?{const?{?data?}?=?el;const?barWidth?=?canvas.width?/?10;const?gap?=?20;const?paddingLeft?=?(data.length?*?barWidth?+?(data.length?-?1)?*?gap)?/?2;const?paddingBottom?=?10;//?x軸//?柱狀圖data.forEach(({?title,?count,?color?},?index)?=>?{const?x?=?paddingLeft?+?index?*?(barWidth?+?gap);const?y?=?canvas.height?-?paddingBottom?-?count;ctx.fillStyle?=?color;ctx.fillRect(x,?y,?barWidth,?count);});}//?遞歸繪制?節點el.childs?&&?el.childs.forEach(child?=>?draw(child,?true)); }; const?createCanvasApp?=?(App)?=>?{const?app?=?renderer.createApp(App);const?{?mount?}?=?app;app.config.isCustomElement?=?(tag)?=>?tag?===?'chart';app.mount?=?(selector)?=>?{canvas?=?document.createElement('canvas');ctx?=?canvas.getContext('2d');document.querySelector(selector).appendChild(canvas);mount(canvas);};return?app; }; createCanvasApp({setup()?{const?data?=?[{?title:?'數據A',?count:?200,?color:?'brown'?},{?title:?'數據B',?count:?300,?color:?'skyblue'?},{?title:?'數據C',?count:?50,?color:?'gold'?},];return?()?=>?h("chart",?{?data?});}, }).mount('#app');運行結果如下圖所示:
Vue3 相關資料
Vue3 官方網站
Vite 官方網站
Vue.js 2021 最新報告
Vue Template Explorer
第四屆 Vue Conf(預計 2021.5.22)
ThisDot 線上分享會 PPT(2020.4.16)
Vue Function-based API RFC(2020.1.22)
State of Vue(2019.6.8)
Vue3 最新進展(2018.11.24)
現狀與展望(2017.5.20)
視頻號最新視頻
5月28-29日
QECon全球軟件質量&效能大會
歡迎關注
總結
以上是生活随笔為你收集整理的升级 Vue3 大幅提升开发运行效率的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大牛书单 | 读书日,他们最近看了这些书
- 下一篇: 神经网络可视化,真的很像神经元!