Vue.js 3.x 優化概覽

来源:https://www.cnblogs.com/CherishTheYouth/archive/2022/04/21/CherishTheYouth_20220421.html
-Advertisement-
Play Games

本文整理自拉勾網Vue.js 3.x 源碼課程,講師是來自Zoom的大牛黃軼,非常感謝! 本人僅補充一些參考資料。 1. Vue.js框架的演進過程 Vue.js 從 1.x 到 2.0 版本,最大的升級就是引入了虛擬 DOM 的概念。 Vue.js 2.x 的版本痛點問題: 源碼自身的維護性; 數 ...


本文整理自拉勾網Vue.js 3.x 源碼課程,講師是來自Zoom的大牛黃軼,非常感謝! 本人僅補充一些參考資料。

1. Vue.js框架的演進過程

Vue.js 從 1.x 到 2.0 版本,最大的升級就是引入了虛擬 DOM 的概念。

image-20220412212335522

Vue.js 2.x 的版本痛點問題:

  • 源碼自身的維護性;
  • 數據量大後帶來的渲染和更新的性能問題;
  • 雞肋 API;
  • TypeScript 支持不佳;
  • ...

Vue.js 3.x 帶來的優化

  • 源碼優化

  • 性能優化

  • 語法Api優化

    image-20220412212300757

2. Vue.js 3.0 優化概覽

那麼接下來,我們就一起來看一下 Vue.js 3.0 具體做了哪些優化。

2.1 源碼優化

首先是源碼優化,也就是祖師爺對於 Vue.js 框架本身開發的優化,它的目的是讓代碼更易於開發和維護。

源碼的優化主要體現在使用 monorepoTypeScript 管理和開發源碼,這樣做的目標是提升自身代碼可維護性。接下來我們就來看一下這兩個方面的具體變化。

2.1.1 更好的代碼管理方式:monorepo

  1. 什麼是monorepo?

就是把多個項目放在一個倉庫裡面,相對立的是傳統的 MultiRepo 模式,即每個項目對應一個單獨的倉庫來分散管理。

img

  1. monorepo 解決什麼問題?
    • 多個repo難以管理,編輯器需要打開多個項目;

    • 某個模塊升級,依賴改模塊的其他模塊需要手動升級,容易疏漏;

    • 公用的npm包重覆安裝,占據大量硬碟容量,比如打包工具webpack會在每個項目中安裝一次;

    • 對新人友好,一句命令即可完成所有模塊的依賴安裝,且整個項目模塊不用到各個倉庫去找;

  2. monorepo 在vue.js 3.x 中的應用

源碼的優化體現在代碼管理方式上。

Vue.js 2.x 的源碼托管在 src 目錄,然後依據功能拆分出了

  • compiler(模板編譯的相關代碼)
  • core(與平臺無關的通用運行時代碼)
  • platforms(平臺專有代碼)
  • server(服務端渲染的相關代碼)
  • sfc(.vue 單文件解析相關代碼)
  • shared(共用工具代碼)

等目錄:

Drawing 0.png

而到了 Vue.js 3.0 ,整個源碼是通過 monorepo 的方式維護的,根據功能將不同的模塊拆分到 packages 目錄下麵不同的子目錄中:

Drawing 1.png

可以看出相對於 Vue.js 2.x 的源碼組織方式,monorepo 把這些模塊拆分到不同的 package 中,每個 package 有各自的 API、類型定義和測試。

這樣做的優勢在於:

  • 使得模塊拆分更細化,職責劃分更明確,模塊之間的依賴關係也更加明確
  • 開發人員也更容易閱讀、理解和更改所有模塊源碼,提高代碼的可維護性。
  • 一些 package(比如 reactivity 響應式庫)是可以獨立於 Vue.js 使用的,這樣用戶如果只想使用 Vue.js 3.0 的響應式能力,可以單獨依賴這個響應式庫而不用去依賴整個 Vue.js,減小了引用包的體積大小,而 Vue.js 2 .x 是做不到這一點的。

參考資料:

2.1.2 有類型的 JavaScript:TypeScript

源碼的優化還體現在 Vue.js 3.0 自身採用了 TypeScript 開發。

Vue.js 1.x 版本的源碼是沒有用類型語言的,祖師爺用 JavaScript 開發了整個框架,但對於複雜的框架項目開發,使用類型語言非常有利於代碼的維護,因為它可以在編碼期間幫你做類型檢查,避免一些因類型問題導致的錯誤;也可以利於它去定義介面的類型,利於 IDE 對變數類型的推導。

因此在重構 2.0 的時候,祖師爺選型了 Flow(Flow是JavaScript代碼的靜態類型檢查器。)。

參考資料:

但是在 Vue.js 3.0 的時候拋棄 Flow 轉而採用 TypeScript 重構了整個項目,這裡有兩方面原因:

首先,Flow 是 Facebook 出品的 JavaScript 靜態類型檢查工具,它可以以非常小的成本對已有的 JavaScript 代碼遷入,非常靈活,這也是 Vue.js 2.0 當初選型它時一方面的考量。但是 Flow 對於一些複雜場景類型的檢查,支持得並不好

其次,Vue.js 3.0 拋棄 Flow 後,使用 TypeScript 重構了整個項目。 TypeScript提供了更好的類型檢查,能支持複雜的類型推導;由於源碼就使用 TypeScript 編寫,也省去了單獨維護 d.ts 文件的麻煩;就整個 TypeScript 的生態來看,TypeScript 團隊也是越做越好,TypeScript 本身保持著一定頻率的迭代和更新,支持的 feature 也越來越多。

2.2 性能優化

2.2.1 源碼體積優化

首先是源碼體積優化,我們在平時工作中也經常會嘗試優化靜態資源的體積,因為 JavaScript 包體積越小,意味著網路傳輸時間越短,JavaScript 引擎解析包的速度也越快。

那麼,Vue.js 3.0 在源碼體積的減少方面做了哪些工作呢?

  1. 移除一些冷門的 feature

    比如 filter、inline-template 等

  2. 引入 tree-shaking 的技術,減少打包體積

    Tree shaking 是一個通常用於描述移除 JavaScript 上下文中的未引用代碼(dead-code) 行為的術語。

    它依賴於ES2015中的 importexport 語句,用來檢測代碼模塊是否被導出、導入,且被 JavaScript 文件使用。

    在現代 JavaScript 應用程式中,我們使用模塊打包(如webpackRollup)將多個 JavaScript 文件打包為單個文件時自動刪除未引用的代碼。這對於準備預備發佈代碼的工作非常重要,這樣可以使最終文件具有簡潔的結構和最小化大小。

    參考資料:

第一點很好理解,所以這裡我們來看看 tree-shaking,它的原理很簡單,tree-shaking 依賴 ES2015 模塊語法的靜態結構(即 import 和 export),通過編譯階段的靜態分析,找到沒有引入的模塊並打上標記。

舉個例子,一個 math 模塊定義了 2 個方法 square(x) 和 cube(x) :

export function square(x) {
  return x * x
}

export function cube(x) {
  return x * x * x
}

我們在這個模塊外面只引入了 cube 方法:

import { cube } from './math.js'
// do something with cube

最終 math 模塊會被 webpack 打包生成如下代碼:

/* 1 */

/***/ (function(module, __webpack_exports__, __webpack_require__) {
  'use strict';
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  function square(x) {
    return x * x;
  }
  function cube(x) {
    return x * x * x;
  }
});

可以看到,未被引入的 square 模塊被標記了, 然後壓縮階段會利用例如 uglify-js、terser 等壓縮工具真正地刪除這些沒有用到的代碼。

也就是說,利用 tree-shaking 技術,如果你在項目中沒有引入 Transition、KeepAlive 等組件,那麼它們對應的代碼就不會打包,這樣也就間接達到了減少項目引入的 Vue.js 包體積的目的。

2.2.2 數據劫持優化

2.2.2.1 數據響應式

Vue.js 區別於 React 的一大特色是它的數據是響應式的,這個特性從 Vue.js 1.x 版本就一直伴隨著,這也是 Vue.js 粉喜歡 Vue.js 的原因之一。

DOM 是數據的一種映射,數據發生變化後可以自動更新 DOM,用戶只需要專註於數據的修改,沒有其餘的心智負擔。

在 Vue.js 內部,想實現這個功能是要付出一定代價的,那就是必須劫持數據的訪問和更新

其實這點很好理解,當數據改變後,為了自動更新 DOM,那麼就必須劫持數據的更新,也就是說當數據發生改變後能自動執行一些代碼去更新 DOM。

那麼問題來了,Vue.js 怎麼知道更新哪一片 DOM 呢?

因為在渲染 DOM 的時候訪問了數據,我們可以對它進行訪問劫持,這樣就在內部建立了依賴關係,也就知道數據對應的 DOM 是什麼了。

以上只是大體的思路,具體實現要比這更複雜,內部還依賴了一個 watcher 的數據結構做依賴管理,參考下圖:

1.png

2.2.2.2 響應式實現方案
  1. Vue.js 1.x 和 Vue.js 2.x 版本

Vue.js 1.x 和 Vue.js 2.x 內部都是通過 Object.defineProperty 這個 API 去劫持數據的 getter 和 setter,具體是這樣的:

Object.defineProperty(data, 'a',{
  get(){
    // track
  },
  set(){
    // trigger
  }
})

但這個 API 有一些缺陷

  • 它必須預先知道要攔截的 key 是什麼,所以它並不能檢測對象屬性的添加和刪除

    var vm = new Vue({
      data: {
        a: 1
      }
    })
    // `vm.a` 現在是響應式的
    vm.b = 2
    // `vm.b` 不是響應式的
    

​ 儘管 Vue.js 為瞭解決這個問題提供了 $set 和 $delete 實例方法,但是對於用戶來說,還是增加了一定的心智負擔。

Vue 2 中更改檢測的註意事項——$set

  • 另外 Object.defineProperty 的方式還有一個問題,舉個例子,比如這個嵌套層級比較深的對象:
export default {
  data: {
    a: {
      b: {
        c: {
          d: 1
        }
      }
    }
  }
}

由於 Vue.js 無法判斷你在運行時到底會訪問到哪個屬性,所以對於這樣一個嵌套層級較深的對象,如果要劫持它內部深層次的對象變化,就需要遞歸遍歷這個對象,執行 Object.defineProperty 把每一層對象數據都變成響應式的。毫無疑問,如果我們定義的響應式數據過於複雜,這就會有相當大的性能負擔

  1. Vue.js 3.x 版本

為瞭解決上述 2 個問題,Vue.js 3.0 使用了 Proxy API 做數據劫持,它的內部是這樣的:

observed = new Proxy(data, {
  get() {
    // track
  },
  set() {
    // trigger
  }
})

由於它劫持的是整個對象,那麼自然對於對象的屬性的增加和刪除都能檢測到。

但要註意的是,Proxy API 並不能監聽到內部深層次的對象變化,因此 Vue.js 3.x 的處理方式是在 getter 中去遞歸響應式,這樣的好處是真正訪問到的內部對象才會變成響應式,而不是無腦遞歸,這樣無疑也在很大程度上提升了性能。Vue.js 3.x 中響應式的實現過程比較複雜,在此不展開講解。

2.2.3 編譯優化

最後是編譯優化,為了便於理解,我們先來看一張圖:

2.png

這是 Vue.js 2.x 從 new Vue 開始渲染成 DOM 的流程,上面說過的響應式過程就發生在圖中的 init 階段,另外 template compile to render function 的流程是可以藉助 vue-loader 在 webpack 編譯階段離線完成,並非一定要在運行時完成。

所以想優化整個 Vue.js 的運行時,除了數據劫持部分的優化,我們可以在耗時相對較多的 patch 階段想辦法,Vue.js 3.0 也是這麼做的,並且它通過在編譯階段優化編譯的結果,來實現運行時 patch 過程的優化。

通過數據劫持和依賴收集,Vue.js 2.x 的數據更新並觸發重新渲染的粒度是組件級的:

3.png

雖然 Vue 能保證觸發更新的組件最小化,但在單個組件內部依然需要遍歷該組件的整個 vnode 樹,舉個例子,比如我們要更新這個組件:

<template>
  <div id="content">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{message}}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</template>

整個 diff 過程如圖所示:

圖片1.png

可以看到,因為這段代碼中只有一個動態節點,所以這裡有很多 diff 和遍歷其實都是不需要的,這就會導致 vnode 的性能跟模版大小正相關,跟動態節點的數量無關,當一些組件的整個模版內只有少量動態節點時,這些遍歷都是性能的浪費。

而對於上述例子,理想狀態只需要 diff 這個綁定 message 動態節點的 p 標簽即可。

Vue.js 3.0 做到了,它通過編譯階段對靜態模板的分析,編譯生成了 Block tree。Block tree 是一個將模版基於動態節點指令切割的嵌套區塊,每個區塊內部的節點結構是固定的,而且每個區塊只需要以一個 Array 來追蹤自身包含的動態節點。藉助 Block tree,Vue.js 將 vnode 更新性能由與模版整體大小相關提升為與動態內容的數量相關,這是一個非常大的性能突破,此過程比較複雜。

除此之外,Vue.js 3.0 在編譯階段還包含了對 Slot 的編譯優化、事件偵聽函數的緩存優化,並且在運行時重寫了 diff 演算法等。

2.3 語法 API 優化:Composition API

除了源碼和性能方面,Vue.js 3.0 還在語法方面進行了優化,主要是提供了 Composition API。

2.3.1 優化邏輯組織

首先,是優化邏輯組織。

在 Vue.js 1.x 和 2.x 版本中,編寫組件本質就是在編寫一個“包含了描述組件選項的對象”,我們把它稱為 Options API,它的好處是在於寫法非常符合直覺思維,對於新手來說這樣很容易理解,這也是很多人喜歡 Vue.js 的原因之一。

Options API 的設計是按照 methodscomputeddataprops 這些不同的選項分類,當組件小的時候,這種分類方式一目瞭然;但是在大型組件中,一個組件可能有多個邏輯關註點,當使用 Options API 的時候,每一個關註點都有自己的 Options,如果需要修改一個邏輯點關註點,就需要在單個文件中不斷上下切換和尋找。

舉一個官方例子 Vue CLI UI file explorer,它是 vue-cli GUI 應用程式中的一個複雜的文件瀏覽器組件。這個組件需要處理許多不同的邏輯關註點:

  • 跟蹤當前文件夾狀態並顯示其內容
  • 處理文件夾導航(比如打開、關閉、刷新等)
  • 處理新文件夾的創建
  • 切換顯示收藏夾
  • 切換顯示隱藏文件夾
  • 處理當前工作目錄的更改

如果我們按照邏輯關註點做顏色編碼,就可以看到當使用 Options API 去編寫組件時,這些邏輯關註點是非常分散的:

Drawing 6.png

Vue.js 3.0 提供了一種新的 API:Composition API,它有一個很好的機制去解決這樣的問題,就是將某個邏輯關註點相關的代碼全都放在一個函數里,這樣當需要修改一個功能時,就不再需要在文件中跳來跳去。

通過下圖,我們可以很直觀地感受到 Composition API 在邏輯組織方面的優勢:

Drawing 7.png

2.3.2 優化邏輯復用

其次,是優化邏輯復用。

當我們開發項目變得複雜的時候,免不了需要抽象出一些復用的邏輯。在 Vue.js 2.x 中,我們通常會用 mixins 去復用邏輯,舉一個滑鼠位置偵聽的例子,我們會編寫如下函數 mousePositionMixin:

const mousePositionMixin = {
  data() {
    return {
      x: 0,
      y: 0
    }
  },
  mounted() {
    window.addEventListener('mousemove', this.update)
  },

  destroyed() {
    window.removeEventListener('mousemove', this.update)
  },
  methods: {
    update(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}
export default mousePositionMixin

然後在組件中使用:

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
  mixins: [mousePositionMixin]
}
</script>

使用單個 mixin 似乎問題不大,但是當我們一個組件混入大量不同的 mixins 的時候,會存在兩個非常明顯的問題:

  • 命名衝突
  • 數據來源不清晰

首先每個 mixin 都可以定義自己的 props、data,它們之間是無感的,所以很容易定義相同的變數,導致命名衝突。

另外對組件而言,如果模板中使用不在當前組件中定義的變數,那麼就會不太容易知道這些變數在哪裡定義的,這就是數據來源不清晰。

但是Vue.js 3.0 設計的 Composition API,就很好地幫助我們解決了 mixins 的這兩個問題。

我們來看一下在 Vue.js 3.0 中如何書寫這個示例:

import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

這裡我們約定 useMousePosition 這個函數為 hook 函數,然後在組件中使用:

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
  import useMousePosition from './mouse'
  export default {
    setup() {
      const { x, y } = useMousePosition()
      return { x, y }
    }
  }
</script>

可以看到,整個數據來源清晰了,即使去編寫更多的 hook 函數,也不會出現命名衝突的問題。

Composition API 除了在邏輯復用方面有優勢,也會有更好的類型支持,因為它們都是一些函數,在調用函數時,自然所有的類型就被推導出來了,不像 Options API 所有的東西使用 this。

另外,Composition API 對 tree-shaking 友好,代碼也更容易壓縮。

3.總結

以上就是Vue.js 3.x 大版本所做的優化,在實際項目開發中,Vue.js 3.x 相對於 Vue.js 2.x 來說,確實能帶來更好的開發體驗和較大的性能提升。

作者:CherishTheYouth 出處:https://www.cnblogs.com/CherishTheYouth/ 聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。對於本博客如有任何問題,可發郵件與我溝通,我的QQ郵箱是:[email protected]
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • OpenHarmony技術日 即將揭幕! 4月25日(星期一)09:00-18:00 與你驚喜相約! ...
  • 前言 ​ 隨著科技以及業務的發展,手勢的應用也越來越普及,因此對於數據採集,我們要考慮如果通過全埋點來實現手勢的採集。 一、手勢識別器 ​ 蘋果為了降低開發者在手勢事件處理方面的開發難度,定義了一個抽象類 UIGestureRecognizer 來協助開發者。UIGestureRecognizer ...
  • 支持轉化事件回傳至華為應用市場商業推廣,便捷歸因,實時調優; 卸載分析新增卸載前路徑分析,深度剖析卸載根因。 查看詳情 新增關鍵幀能力,通過關鍵幀點可實現圖片、文字等位置移動、旋轉等動態效果,比如文字的彈幕效果; 新增文字氣泡和花字,快速添加實現不同風格的文字樣式。 查看詳情 新增UI自定義菜單和設 ...
  • 經過前面九天的學習,對Node.js開發有了一個初步的認識,今天繼續學習Node.js後端開發框架Express相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 前言 在 3D 游戲中,都會有一個主人公。我們可以通過點擊游戲中的其他位置,使游戲主人公向點擊處移動。 那當我們想要實現一個“點擊地面,人物移動到點擊處”的功能,需要什麼前置條件,並且具體怎麼實現呢?本文帶大家一步步實現人物行走移動,同時進行狀態改變的功能。 一、骨骼動畫 骨骼動畫(Skeleton ...
  • 本文記錄,JavaScript 中 querySelector 的使用方法。小白貢獻,語失莫怪。 // 兩種 query 的 method (方法) document.querySelector(selectors); document.querySelectorAll(selectors); 為什 ...
  • 立即執行函數(function(){})() 關鍵詞: 立即執行函數 匿名函數 作用域 格式: (function())() 作用: 立即執行函數(function())()是匿名函數, 主要利用函數的作用域, 減少全局變數的使用 解釋 前提知識 js中的函數通常有兩種命名方式,分別是 聲明式函數 ...
  • 為什麼要使用Vue Vue框架誕生於2014年,其作者為中國人——尤雨溪,也是新人最容易入手的框架之一,不同於React和Angular,其中文文檔也便於大家閱讀和學習。Vue用於構建互動式的Web界面的庫,是一個用於構建用戶界面的漸進式框架。與其他單體框架不同,Vue 從頭開始設計為可逐步採用。核 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...