前言
2020年初,vue3 發布了第一個版本,在隨后的時間內,vue-next
一直保持著快速的更新,直到去年的 9.18 號發布了第一個版本 one piece
,這個備受關注的庫帶來了更小的體積,更好的類型支持以及一套能夠更優雅地處理復雜邏輯的api。并且由于苦于 webpack
在開發模式下,熱模塊替換隨著項目變大速度也會變慢,基于原生的 es module
開發出了一套新的打包工具 vite
。
本文會從宏觀角度來拆解 vue3。vue3 主要分為以下三個核心模塊:響應式、編譯器及運行時。
響應式
vue3 的響應式系統著重解決了兩個問題:
- vue3 減少了實例化組件帶來的性能開銷
- 提供了一種 vue2 所缺少全局狀態共享方法
我們知道,在 vue2 中創建組件實例的時候,我們往往需要在組件的實例上也就是 this 上暴露很多屬性,data、props、methods 等等,這些屬性都是要通過基于 es5 的 Object.defineProperty
這個 api 來掛載在 this上,這個操作是比較耗時的。
而在新版本的響應式系統基于proxy進行實現,我們就可以把上述屬性掛載過程丟棄掉,暴露給渲染函數的this實際上是一個 proxy
,我們要取值的時候,由于前置已經知道了這個屬性是 data、還是 methods,就可以直接從 proxy
上動態返回,省去了提前定義的這個步驟。
其次,由于 vue2 中缺少一種全局狀態的共享方法,雖然可以通過諸如provide
、inject
、event bus
這種方式進行,但是在業務中,還是不是很好用,就得考慮使用 vuex。
我們可以知道,要使得數據能夠全局共享,需要滿足兩個基本要求:一數據要具備響應式能力,即當其發生變化時候,要同時依賴于他的數據進行更新。其次滿足可用且單例,可用性比較直觀,數據一定是被導出的才能被別的組件所應用,單例指的是這個狀態是全局唯一的,不能存在多份數據不一致的情況。
而 vue3 提供的獨立成包的 @vue/reactive
就具備上述能力,這里有一個簡單的 demo,分為兩部分,上面子組件,下面父組件,每個組件都沒有在組件內部定義數據,所有組件的數據都是由store進行共享的。
// store.js
import { reactive } from "vue";
const createStore = (store) => reactive(store);
const baseStore = {
count: 0,
data: null
};
const store = createStore(baseStore);
const dispatch = (type, payload) => (store[type] = payload);
export const useStore = () => {
return {
store,
dispatch
};
};
export default store;
在 store 中做的事情就比較簡單,我們把一個普通的對象通過 reactive
這個 api 包裹使其具有響應式的能力。我們在父組件點擊改變 store 的值,子組件也能夠實現狀態的同步。
同時,我們在子組件內部模擬一個異步請求,等待一秒鐘后父組件也能夠同步從子組件中獲取的數據。我們可以看到,在子組件內部是直接通過store.count++
來修改數據的,這種方式其實在多人協作及項目復雜的時候是不可控的,狀態流轉是不清楚的。那么我們可以在 store 的基礎上進行增強,來定義一個 dispatch
方法,使用 useStore
來自定義一個 hook
,將其暴露出去,在組件內部使用 dispatch
來進行數據操作。
以上我們就通過簡單的響應式 api 實現了一個簡單的數據共享的模型。那么vuex 的存在意義是什么?
其意義在于保證狀態修改及副作用的可控性,是一種用制約來換取可維護性的一種平衡策略。vuex 實際上是以一種插件形式存在,在插件內部就可以實現一些派發和監聽事件,來作為我們常用的調試工具進行狀態的回滾及查看操作。未來的 vuex api 會在響應式模塊的基礎上進行大幅度減化。
同時,由于 @vue/reactive
獨立成包,因此可以被用于其他框架進行狀態控制。下面推薦了兩篇文章及一個開源庫 reactivue 都是將響應式 api 集成到 React Hooks
中,在react 中,修改狀態不再需要使用 setState
,而是直接修改狀態。其中的核心都是在依賴收集的 effect
函數內,強制修改 react 內部的狀態,這樣子就能夠做到當依賴發生變化時候,effect
函數重新執行,同時 React 內部的 state 發生變化,函數組件就會重新執行。
編譯器
接下來我們看一下vue的編譯器,編譯器做的工作就是把單文件組件的 template 模板編譯成 render 函數,具有同樣能力的有 webpack 中的 vue-loader
。render 函數的定義如下圖所示,實質上渲染函數返回的是虛擬dom,也就是一個純 JavaScript 對象。
function h() {
return {
_isVNode: true,
flags: VNodeFlags.ELEMENT_HTML,
tag: 'h1',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN,
el: null
}
}
vue3 的編譯器相比于 vue2 做了許多性能上的提升:
- 在寫 template 的時候,我們可以把模板中的節點進行分類:動態節點和靜態節點。動態節點指的是與數據掛鉤及有邏輯操作的節點,靜態節點就是內容固定的節點。vue在將節點進行編譯的時候就將 template 中的節點進行動靜態區分,對于靜態節點,在渲染的之前,就會把節點的定義置頂放在 render 函數的外面,無需每次 rerender 去重新定義,相當于做了緩存。
- render 函數有第二個參數,用于存放組件的屬性信息,相對于 vue2,vue3 會把該參數的對象進行打平。我們通過一個實例來具體看看編譯器所做的優化。
<div id="app">
<div v-if="msg">{{msg}}</div>
<div v-else>pending...</div>
<template v-for="item in list" :key="item">
<div>{{item.name}}</div>
</template>
<comp title="hello" class="haha" />
<div>static element</div>
</div>
這里有一段代碼,我們分別把它放到 vue2 和 vue3 的編譯器中得到編譯后的渲染函數。這段模板分為四個部分,分別是 v-if
的動態區塊,v-for
的動態區塊,自定義組件區塊及靜態區塊。我們在 options 里面勾選 hoistStatic
就能夠看到編譯器把靜態節點的創建做了提升,同時會把能做提前預定義的部分都做抽離。
對比 vue2 的編譯器,我們格式化 with 語句之后,可以看出無論是靜態節點還是動態節點,都會在渲染函數中定義,同時渲染函數的第二個參數具有較深的層級,而 vue3 的渲染函數的第二個參數對象層級只有一層。另外的一些細節可以看出,vue3 的 template 不再限制根節點的數量,同時 v-for
的 key 是可以綁定在 template 上的,vue2 只能綁定在實體節點上。
運行時
運行時是 vue 中的一個核心模塊。我們知道 vue 寫的程序是可以跑在 web端、小程序及原生app上,其跨平臺的核心支持之一就是其運行時模塊中的渲染器。
- mpvue:美團開發的小程序框架,readme中介紹其是fork了vue的源碼,并且增強了運行時和編譯器的能力。
- 在 vue2 的源碼目錄結構中可以看到,platform目錄下有web和weex兩個文件夾,里面分別有compiler和runtime的部分,web端還有關于服務端渲染的內容。
這個是因為vue2的歷史原因,沒有設計關于平臺渲染相關的api。因此vue3在運行時模塊中,有一部分自定義渲染器runtime-test,通過給渲染器配置不同平臺的渲染操作的選項,就可以把vue程序跑在不同平臺的應用上。
針對web開發,最常用的模塊就是web相關的模塊 runtime-dom
。我們知道在 vue3 中創建應用的時候,是利用了 vue3 暴露出來的 createApp
這個api,把根組件作為參數傳遞進去,然后掛載在真實的 dom 容器上。
function createApp(rootComponent) {
const app = ensureRenderer().createApp(rootComponent)
const {mount} = app
// 重寫與 dom 有關的 mount 方法
app.mount = function(container) {
if (!container) return
container.innerHTML = ''
const proxy = mount(container)
return proxy
}
return app
}
createApp
的實現偽代碼所示,我們可以看到 createApp
返回的 app是由一個 ensureRenderer
方法調用其返回值中的 createApp
方法得到的,而 ensureRenderer
這個方法返回了由 runtime-core
這個模塊提供的與渲染平臺邏輯無關的基礎渲染器,在渲染器的基礎上創建渲染實例。渲染實例里面存在一個 mount
方法,由于渲染平臺是web,我們就重寫與dom 相關的 mount
方法,并且重寫的 mount
方法會調用原始的 mount
方法。
我們上述描述的過程可以用這張圖來展示,runtime-dom
中的ensureRender
調用的來自 runtime-core
提供的 baseCreateRender
方法,在這個過程中我們需要傳遞給渲染器平臺相關的渲染邏輯,web平臺就需要傳入 dom 操作方法,如 createElement
,removeChild
等。其返回值是一個包含了 render
方法和 createApp
方法的對象。createApp
返回了應用實例 app
,里面包含了原始的 mount
方法。
關于 vue3 的討論
以上我們分析完了 vue3 的三個重要模塊。接下來我們來看一些關于vue3的討論。
- 第一個就是社區內爭議比較大的
Ref
語法糖,可以幫助開發者節省代碼冗余,但是迎來了一些負面評價,增加了學習和理解成本,在實際團隊開發中,完全可以使用團隊規范來進行制約是否使用該語法糖。 - 其次就是 vue3 為什么不用
class based api
,因為社區內有了class api 加裝飾器的 ts 方案,據尤大介紹說,不考慮使用該語法的原因有:- 支持
mixin
困難,由于 vue 升級要考慮用戶的使用習慣,不會拋棄mixin
語法 - 漸進式升級,不拋棄之前的 api 使用方法,class 語法與options api 對應起來比較困難
- class語法需要裝飾器的能力增強,但是裝飾器語法的es提案沒有完全確定
- 支持
- 第三個討論就是 vue3 的
composition api
和React Hooks
很像,接下來我們聊聊這塊的話題。
與 React Hooks 對比
隨著應用復雜程度增加,組件的邏輯復用在開發中十分關鍵。目前在 react和 vue 中有以下幾種邏輯復用方案:
mixin
- 兩個由社區提供的方案,
HOC
、Render props
vue 中高階組件是可以應用的,但是由于 vue 插槽機制等原因,高階組件不太常用也不太好用。Render props
可以以作用域插槽的形式應用。最后就是 hooks 這種方法。
在 composition api
出現前,vue 邏輯復用只有 mixin
一種方式,隨著項目變得復雜,處理一個邏輯點的代碼可能分散在多個 mixin
或者是代碼塊中,難以維護,因此 compsition api
的出現就能夠使得邏輯點集中,易于維護。
尤大也是承認 composition api
的設計是受了react hooks 的啟發,但是由于兩個框架的運行機制不同,很多相似更多是代碼書寫方式上的。要比較兩個語法,就必須提到社區內被提到比較多的詞:”心智負擔“。心智負擔指的是新事物的出現可能會與人們的以往認知不一致的情況,這就增加了人們認知上的成本。事實上這兩種語法都是會存在心智負擔的。
vue 的心智負擔只要集中在 ref
和 reactive
上,通過 reactive
包裹一個對象就能夠將其變成響應式的,而 ref
的設計只暴露一個屬性 value
,值為本身。
因此 ref
的實現有以下兩種方式:
function ref(initState) {
return reactive({
value: initState
});
}
// 利用了對象的訪問器
function ref(raw) {
const r = {
get value() {
track(r, "value");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(r, "value");
}
};
return r;
}
第一種方式直接用 reactive
包裹一個包含 value
屬性的對象,第二種實現利用了對象的訪問器。由于 reactive
可以增加屬性,違背了 ref
的初衷。并且在 vue3 中還暴露了 isRef
這個api來判斷該對象是否是 ref
對象,因此 ref
的定義上會給對象增加一些內部屬性。因此第二種方式才是真正實現 ref
的。同時 ref
相對于 reactive
性能更好,因為在把一個對象定義為 reactive
之前,要做很多邏輯判斷。
同時我們在寫 setup
函數的時候一定要記得把響應式對象和定義的方法return 出去,解構一個響應式對象的時候會使其喪失響應式的能力。以上就是 vue 中存在的心智負擔。
由于 react hooks
是以函數形式定義的組件,由于函數天然存在的閉包特性,會導致 hooks 在使用過程中會存在很多需要注意的問題。包括函數組件內部使用定時器導致的舊值輸出、useEffect
、useMemo
需要依賴正確的值,useCallback
做函數引用優化向子組件傳遞可能會導致函數內依賴舊值等等一系列問題。總而言之,就是開發者需要盡可能減少不必要的組件re-render。在 vue 中,響應式系統會自動處理依賴關系,所以不會存在引用舊值的問題。
同時,在寫 vue 的時候,開發者很少會去關注組件的性能優化,由于 vue 框架自身做了很多工作,比如 vue 的響應式依賴收集只有在 effect
內部才會去做,因此開發者做 vue 的性能優化的時候主要集中在盡可能避免定義不必要的響應式數據以及減少不必要的依賴收集。而在 react 中,性能優化對于大型項目是不可或缺的。
但是也不得不說,react hooks
是偉大的設計。
jsx 與 template
關于vue3最后一部分,我們來說一說模板和 jsx。不論是模板還是 jsx 都是編寫視圖的一種方式,實際上有很多用 jsx 開發 vue 項目的應用,很早以前也存在著 react-template
這種 react 模板的方式。目前主流的 SFC 之于vue 和 jsx 之于 react 都是沉淀下來的最佳實踐。
jsx 具有較強的動態性,靈活性強;模板是靜態的,直觀易懂。實質上,無論是模板還是 jsx 都是需要被編譯的,react 中的 jsx 被編譯成為 createElement
,vue中的模板被編譯成渲染函數。在 vue3 中配合 jsx,確實能夠享受到寫純 JavaScript 的流暢感,也有良好的 ts 類型支持和靜態屬性檢查機制,并且可以在一個js文件中定義多個組件。
但是隨著vue3周邊生態的成熟,在 vscode 中配合 volar
插件,也能夠在模板中做組件屬性的靜態類型檢查,體驗還是不錯的,但是貌似會略微造成電腦卡頓。同時,由于模板做了很多編譯優化,因此在性能上優于 jsx。
總結
對于前端開發從業者而言,前端開發三大框架之一的 vue 的大版本更新絕對是重磅消息,隨之而來的便是面向新版本的新生態系統的建立。vue3 繼承自vue2,也著重解決了之前存在的一些痛點。對于我們開發者來說,了解 vue3 的內部細節也是必要的。