Vue3和組件那些事

前言

2020年初,vue3 發布了第一個版本,在隨后的時間內,vue-next 一直保持著快速的更新,直到去年的 9.18 號發布了第一個版本 one piece,這個備受關注的庫帶來了更小的體積,更好的類型支持以及一套能夠更優雅地處理復雜邏輯的api。并且由于苦于 webpack 在開發模式下,熱模塊替換隨著項目變大速度也會變慢,基于原生的 es module 開發出了一套新的打包工具 vite

本文會從宏觀角度來拆解 vue3。vue3 主要分為以下三個核心模塊:響應式、編譯器及運行時。

vue3總覽

響應式

vue3 的響應式系統著重解決了兩個問題:

  • vue3 減少了實例化組件帶來的性能開銷
  • 提供了一種 vue2 所缺少全局狀態共享方法

我們知道,在 vue2 中創建組件實例的時候,我們往往需要在組件的實例上也就是 this 上暴露很多屬性,data、props、methods 等等,這些屬性都是要通過基于 es5 的 Object.defineProperty 這個 api 來掛載在 this上,這個操作是比較耗時的。

而在新版本的響應式系統基于proxy進行實現,我們就可以把上述屬性掛載過程丟棄掉,暴露給渲染函數的this實際上是一個 proxy,我們要取值的時候,由于前置已經知道了這個屬性是 data、還是 methods,就可以直接從 proxy 上動態返回,省去了提前定義的這個步驟。

其次,由于 vue2 中缺少一種全局狀態的共享方法,雖然可以通過諸如provideinjectevent 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 就能夠看到編譯器把靜態節點的創建做了提升,同時會把能做提前預定義的部分都做抽離。

圖片 4
圖片 5

對比 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 方法。

圖片 3

我們上述描述的過程可以用這張圖來展示,runtime-dom 中的ensureRender 調用的來自 runtime-core 提供的 baseCreateRender 方法,在這個過程中我們需要傳遞給渲染器平臺相關的渲染邏輯,web平臺就需要傳入 dom 操作方法,如 createElementremoveChild 等。其返回值是一個包含了 render 方法和 createApp 方法的對象。createApp 返回了應用實例 app,里面包含了原始的 mount 方法。

關于 vue3 的討論

以上我們分析完了 vue3 的三個重要模塊。接下來我們來看一些關于vue3的討論。

  • 第一個就是社區內爭議比較大的 Ref 語法糖,可以幫助開發者節省代碼冗余,但是迎來了一些負面評價,增加了學習和理解成本,在實際團隊開發中,完全可以使用團隊規范來進行制約是否使用該語法糖。
  • 其次就是 vue3 為什么不用 class based api,因為社區內有了class api 加裝飾器的 ts 方案,據尤大介紹說,不考慮使用該語法的原因有:
    1. 支持 mixin 困難,由于 vue 升級要考慮用戶的使用習慣,不會拋棄 mixin 語法
    2. 漸進式升級,不拋棄之前的 api 使用方法,class 語法與options api 對應起來比較困難
    3. class語法需要裝飾器的能力增強,但是裝飾器語法的es提案沒有完全確定
  • 第三個討論就是 vue3 的 composition apiReact Hooks 很像,接下來我們聊聊這塊的話題。

與 React Hooks 對比

隨著應用復雜程度增加,組件的邏輯復用在開發中十分關鍵。目前在 react和 vue 中有以下幾種邏輯復用方案:

  1. mixin
  2. 兩個由社區提供的方案,HOCRender props

vue 中高階組件是可以應用的,但是由于 vue 插槽機制等原因,高階組件不太常用也不太好用。Render props 可以以作用域插槽的形式應用。最后就是 hooks 這種方法。

composition api 出現前,vue 邏輯復用只有 mixin 一種方式,隨著項目變得復雜,處理一個邏輯點的代碼可能分散在多個 mixin 或者是代碼塊中,難以維護,因此 compsition api 的出現就能夠使得邏輯點集中,易于維護。

尤大也是承認 composition api 的設計是受了react hooks 的啟發,但是由于兩個框架的運行機制不同,很多相似更多是代碼書寫方式上的。要比較兩個語法,就必須提到社區內被提到比較多的詞:”心智負擔“。心智負擔指的是新事物的出現可能會與人們的以往認知不一致的情況,這就增加了人們認知上的成本。事實上這兩種語法都是會存在心智負擔的。

vue 的心智負擔只要集中在 refreactive 上,通過 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 在使用過程中會存在很多需要注意的問題。包括函數組件內部使用定時器導致的舊值輸出、useEffectuseMemo 需要依賴正確的值,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。

圖片 7

總結

對于前端開發從業者而言,前端開發三大框架之一的 vue 的大版本更新絕對是重磅消息,隨之而來的便是面向新版本的新生態系統的建立。vue3 繼承自vue2,也著重解決了之前存在的一些痛點。對于我們開發者來說,了解 vue3 的內部細節也是必要的。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容