前言
今年又是一個非常寒冷的冬天,很多公司都開始人員精簡,市場從來不缺前端,但對高級前端的需求還是特別強烈的,一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試程序中考察一些前端框架的原始碼性知識點,Vuejs 作為世界頂尖的框架之一,幾乎在所有的面試場景中或多或少都會被提及,
筆者之前在螞蟻集團就職,對于 Vue 3 的考點還是會經常問的,接下來,我將根據多年的面試以及被面試經驗,為小伙伴們梳理最近大廠愛問的 Vue 3 問題,然后我們再根據問題舉一反三,深入學習 Vue 3 原始碼知識!
場景一:Vue 3.x 相對于 Vue 2.x 做了那些額外的性能優化?
要理解 Vue 3 的性能優化的核心,就需要了解 Vuejs 的核心設計理念,我們知道 Vuejs 官網上有一句話總結的特別到位:漸進式 JavaScript 框架,易學易用,性能出色,適用于場景豐富的 Web 框架, 其實我們的答案就蘊藏在這句話里,
首先,我們知道當我們瀏覽 Web 網頁時,有兩類場景會制約 Web 網頁的性能
- 網路傳輸的瓶頸
- 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 定了一個 getter 在 getter 中觸發了很多不可預期的操作,
如果我們確認 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 中,每個組件都會生成一個渲染函式,這些渲染函式執行時會進行資料訪問,此時這些渲染函式被收集進入副作用函式中,建立資料 -> 副作用的映射關系,當資料變更時,再觸發副作用函式的重新執行,即重新渲染,
另一方面則在編譯器中做了大量的靜態優化,得益于這些優化,才讓我們可以 易學易用的寫出性能出色的 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 技術揭秘》,
另外,附上小冊 50 個 6 折碼:jQqasaoW
數量有限,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/541765.html
標籤:其他
