從面試題入手,暢談 Vue 3 性能優化

来源:https://www.cnblogs.com/muwoo/archive/2022/12/20/16994204.html
-Advertisement-
Play Games

前言 今年又是一個非常寒冷的冬天,很多公司都開始人員精簡。市場從來不缺前端,但對高級前端的需求還是特別強烈的。一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試過程中考察一些前端框架的源碼性知識點。Vuejs 作為世界頂尖的框架之一,幾乎在所有的面試場景中或多或少都會被提及。 筆者之前 ...


前言

今年又是一個非常寒冷的冬天,很多公司都開始人員精簡。市場從來不缺前端,但對高級前端的需求還是特別強烈的。一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試過程中考察一些前端框架的源碼性知識點。Vuejs 作為世界頂尖的框架之一,幾乎在所有的面試場景中或多或少都會被提及。

筆者之前在螞蟻集團就職,對於 Vue 3 的考點還是會經常問的。接下來,我將根據多年的面試以及被面試經驗,為小伙伴們梳理最近大廠愛問的 Vue 3 問題。然後我們再根據問題舉一反三,深入學習 Vue 3 源碼知識!

場景一:Vue 3.x 相對於 Vue 2.x 做了那些額外的性能優化?

要理解 Vue 3 的性能優化的核心,就需要瞭解 Vuejs 的核心設計理念。我們知道 Vuejs 官網上有一句話總結的特別到位:漸進式 JavaScript 框架,易學易用,性能出色,適用於場景豐富的 Web 框架。 其實我們的答案就蘊藏在這句話里。

首先,我們知道當我們瀏覽 Web 網頁時,有兩類場景會制約 Web 網頁的性能

  1. 網路傳輸的瓶頸
  2. CPU的瓶頸

所以要回答這個問題,就可以直接從這兩方面入手。

網路傳輸的瓶頸優化

對於前端框架而言,制約網路傳輸的因素最大的就是代碼體積,代碼體積越大,傳輸效率越慢。尤其對於 SPA 單頁應用的 CSR(客戶端渲染) 而言。一個大體積的框架資源,就意味著用戶需要等待白屏的時間越長。而 Vue 3 在減少源碼體積方面做的最多的就是通過精細化的 Tree-Shacking 機制來構建 漸進式 代碼。

1. /*#__PURE__*/ 標記

我們知道 Tree-Shaking 可以刪除一些 DC(dead code) 代碼。但是對於一些有副作用的函數代碼,卻是無法進行很好的識別和刪除,舉個例子:

foo()

function foo(obj) {
  obj?.a
}

上述代碼中,foo 函數本身是沒有任何意義的,僅僅是對對象 obj 進行了屬性 a 的讀取操作,但是 Tree-Shaking 是無法刪除該函數的,因為上述的屬性讀取操作可能會產生副作用,因為 obj 可能是一個響應式對象,我們可能對 obj 定了一個 gettergetter 中觸發了很多不可預期的操作。

如果我們確認 foo 函數是一個不會有副作用的純凈的函數,那麼這個時候 /*#__PURE__*/ 就派上用場了,其作用就是告訴打包器,對於 foo 函數的調用不會產生副作用,你可以放心地對其進行 Tree-Shaking

另外,值得一提的是,在 Vue 3 源碼中,包含了大量的 /*#__PURE__*/ 標識符,可見 Vue 3 對源碼體積的控制是多麼的用心!

2. 特性開關

Vue 3 源碼中的 rollup.config.mjs 中有這樣一段代碼:

{
  __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
}

其中 __FEATURE_OPTIONS_API__ 是一個構建時的環境變數,我們知道 Vue 3 在某些 API 方面是相容 Vue 3 寫法的,比如 Options API。但是如果我們在項目中僅僅使用 Compositon API 而不想使用 Options API 那麼我們就可以在項目構建時關閉這個選項,從而減少代碼體積。我們看看這個變數在 Vue 3 源碼中是如何使用的:

// 相容 2.x 選項式 API
if (__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

用戶可以通過設置 __VUE_OPTIONS_API__ 預定義常量的值來控制是否要包含這段代碼。通常用戶可以使用 webpack.DefinePlugin 插件來實現:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: JSON.stringify(true) // 開啟特性
})

除此之外,類似的開發環境會通過 __DEV__ 來輸出告警規則,而在生產環境剔除這些告警降低構建後的包體積都是類似的手段:

if (__DEV__) {
  console.warn(`value cannot be made reactive: ${String(target)}`)
}

CPU 瓶頸優化

當項目變得龐大、組件數量繁多時,就容易遇到CPU的瓶頸。主流瀏覽器刷新頻率為60Hz,即每(1000ms / 60Hz)16.6ms瀏覽器刷新一次。

我們知道,JS可以操作DOM,GUI渲染線程JS線程是互斥的。所以JS腳本執行瀏覽器佈局、繪製不能同時執行。

在每16.6ms時間內,需要完成如下工作:

JS腳本執行 -----  樣式佈局 ----- 樣式繪製

當JS執行時間過長,超出了16.6ms,這次刷新就沒有時間執行樣式佈局樣式繪製了,也就出現了丟幀的情況,會發生卡頓。

為瞭解決龐大元素組件渲染、更新卡頓的問題,Vue 的策略是一方面採用了組件級的細粒度更新,控制更新的影響面:Vue 3 中,每個組件都會生成一個渲染函數,這些渲染函數執行時會進行數據訪問,此時這些渲染函數被收集進入副作用函數中,建立數據 -> 副作用的映射關係。當數據變更時,再觸發副作用函數的重新執行,即重新渲染。

image.png

另一方面則在編譯器中做了大量的靜態優化,得益於這些優化,才讓我們可以 易學易用的寫出性能出色的 Vue 項目。 下麵簡單介紹幾種編譯時優化策略:

1. 靶向更新

假設有以下模板:

<template>
  <p>hello world</p>
  <p>{{ msg }}</p>
</template>>

其中一個 p 標簽的節點是一個靜態的節點,第二個 p 標簽的節點是一個動態的節點,如果當 msg 的值發生了變化,那麼理論上肉眼可見最優的更新方案應該是只做第二個動態節點的 diff 而無需進行第一個 p 標簽節點的 diff

上述模版轉成 vnode 後的結果大致為:

const vnode = {
  type: Symbol(Fragment),
  children: [
    { type: 'p', children: 'hello world' },
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 動態的 text */ },
  ],
  dynamicChildren: [
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 動態的 text */ },
  ]
}

此時組件記憶體在了一個靜態的節點 <p>hello world</p>,在傳統的 diff 演算法里,還是需要對該靜態節點進行不必要的 diff

Vue3 則是先通過 patchFlag 來標記動態節點 <p>{{ msg }}</p> 然後配合 dynamicChildren 將動態節點進行收集,從而完成在 diff 階段只做靶向更新的目的。

2. 靜態提升

接下來,我們再來說一下,為什麼要做靜態提升呢? 如下模板所示:

<div>
  <p>text</p>
</div>

在沒有被提升的情況下其渲染函數相當於:

import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("p", null, "text")
  ]))
}

很明顯,p 標簽是靜態的,它不會改變。但是如上渲染函數的問題也很明顯,如果組件記憶體在動態的內容,當渲染函數重新執行時,即使 p 標簽是靜態的,那麼它對應的 VNode 也會重新創建。

所謂的 “靜態提升”,就是將一些靜態的節點或屬性提升到渲染函數之外。如下麵的代碼所示:

import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "text", -1 /* HOISTED */)
const _hoisted_2 = [
  _hoisted_1
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}

這就實現了減少 VNode 創建的性能消耗。

而這裡的靜態提升步驟生成的 hoists,會在 codegenNode 會在生成代碼階段幫助我們生成靜態提升的相關代碼。

預字元串化

Vue 3 在編譯時會進行靜態提升節點的 預字元串化。什麼是預字元串化呢?一起來看個示例:

<template>
  <p></p>
  ... 共 20+ 節點
  <p></p>
</template>

對於這樣有大量靜態提升的模版場景,如果不考慮 預字元串化 那麼生成的渲染函數將會包含大量的 createElementVNode 函數:假設如上模板中有大量連續的靜態的 p 標簽,此時渲染函數生成的結果如下:

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
// ...
const _hoisted_20 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
const _hoisted_21 = [
  _hoisted_1,
  // ...
  _hoisted_20,
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

createElementVNode 大量連續性創建 vnode 也是挺影響性能的,所以可以通過 預字元串化 來一次性創建這些靜態節點,採用 與字元串化 後,生成的渲染函數如下:

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p></p>...<p></p>", 20)
const _hoisted_21 = [
  _hoisted_1
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

這樣一方面降低了 createElementVNode 連續創建帶來的性能損耗,也側面減少了代碼體積。

小結

本小節為大家解讀了部分 Vue 3 性能優化的設計,更多的內容可以參考作者寫的小冊:《Vue 3 技術揭秘》

接下來的文章將繼續為大家解讀 Vue 3 響應式設計原理和非同步調度更新策略。

image.png

推廣自己的小冊

如果你對 Vue 3 感興趣,想去深耕一下 Vue 3 相關的設計理念,但是直接去啃 Vue 3 源碼會非常晦澀難懂,比如一個 baseCreateRenderer 函數就有接近 2000 行代碼,可能會讓你半途而廢。

作者花了 3 個多月的時間嘔心瀝血的寫了一個小冊《Vue 3 技術揭秘》將會為您從頭到尾的介紹 Vue 3 的優秀設計!

小冊一方面會對 Vue 3 核心源碼做適量的精簡,讓你可以只用關註核心邏輯實現;另一方面,也配了大量的插圖,一圖勝千言,可以更加生動地向你展示源碼的運行機制。

本小冊主要劃分為了 5 大模塊 來依次為你揭開 Vue 3 的“神秘面紗”。

  • 模塊一:渲染器實現原理。從根組件初始化開始,一步步介紹組件實例化、完整更新、diff 過程等。

  • 模塊二:響應式原理。核心介紹 Vue 3 基於 Proxy 實現的響應式原理,深入解讀依賴收集過程、響應式觸達過程和相關聯的 watch、computed、inject/provide 函數實現以及非同步批量更新原理。在學習的過程中,你會漸進式體會到與 Vue 2 響應式原理的差異以及非同步批量更新的不同之處。

  • 模塊三:編譯器實現原理。重點講解模板是如何被一步步編譯成渲染函數的,以及在編譯時 Vue 3 所做的大量編譯時優化的工作。

  • 模塊四:內置組件實現原理。主要介紹 Vue 3 幾個常用的內置組件:Transition、KeepAlive、Teleport 、Suspense 相關的組件運行機制和實現原理。

  • 模塊五:特殊元素&指令。重點分析 v-model 是如何實現雙向數據綁定的,以及 slot 插槽是如何實現內容分發的。

為方便你理解,我整理出來瞭如下的思維導圖:

image.png

相信掌握了本小冊這些模塊的核心原理之後,你再去閱讀 Vue 3 源碼或者是解決 Vue 3 的疑難雜症時,會更加得心應手。

image.png
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • “數據湖”、“湖倉一體”及“流批一體”等概念,是近年來大數據領域熱度最高的辭彙,在各大互聯網公司掀起了一波波的熱潮,各家公司紛紛推出了自己的技術方案,其中作為全鏈路數字化技術與服務提供商的袋鼠雲,在探索數據湖架構的早期,就調研並選用了Iceberg作為基礎框架,在落地過程中深度使用了Iceberg並 ...
  • 摘要:openGemini的設計和優化都是根據時序數據特點而來,在面對海量運維監控數據處理需求時,openGemini顯然更加有針對性。 IT運維誕生於最早的信息化時代。在信息化時代,企業的信息化系統,主要為了滿足企業內部管理的需求。通常是集中、可控和固化的煙囪式架構。傳統IT運維,以人力運維為主, ...
  • 說說幾種薅免費伺服器羊毛的方法吧 經過我多年的薅羊毛經驗,總結得知,編程只需要: Terminal + VPS主機 + 網路 為了達到這些目的,肯定需要Vim搭建IDE,安裝環境等等操作 當時記得使用的是ChromeOS的筆記本,所有的應用都是在Chrome瀏覽器當中的,續航時間長,又輕薄屬實快得飛 ...
  • 主要闡述InnoDB存儲引擎(MySQL5以後的預設引擎)。 資料庫中最基本的組成結構是數據表,視覺上的表和其對應的磁碟結構如下: 此圖參考了廈門大學課堂:MySQL原理 。但是視頻中一些更多細節沒有涉及,比如Leaf node segment和Non-leaf node segment其實就是葉子 ...
  • 在先前舉辦的華為開發者大會2022(HDC)上,華為通過3D數字溪村展示了自有3D引擎“HMS Core 3D Engine”(以下簡稱3D Engine)的強大能力。作為一款高性能、高畫質、高擴展性的3D引擎,3D Engine不僅能通過實時光追、水體渲染、體積雲霧、多維GPU粒子系統等技術還原真 ...
  • 好家伙,本篇為《JS高級程式設計》第八章“對象、類與面向對象編程”學習筆記 1.關於對象 ECMA-262將對象定義為一組屬性的無序集合。嚴格來說,這意味著對象就是一組沒有特定順序的值。 對象的每個屬性或方法都由一個名稱來標識,這個名稱映射到一個值。正因為如此(以及其他還未討論的原因),可以把 EC ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 不想看繁瑣步驟的,可以直接去github下載項目,如果可以順便來個star哈哈 本項目使用vue-cli創建,但不影響使用,主要繪製都已封裝成類 1、使用geoJson繪製3d地圖 1.1 創建場景相關 // 創建webGL渲染器 thi ...
  • “像白天不懂夜的黑,像永恆燃燒的太陽,不懂那月亮的盈缺。” —— 黃桂蘭 0x00 大綱 0x01 前言 夜間模式(Dark Mode),也被稱為黑暗模式或深色模式,是一種高對比度,或者反色模式的顯示模式,這種模式現在越來越流行,因為和傳統的白底黑字相比,這種黑底白字的模式通常被認為可以緩解眼疲勞, ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...