今年又是一個非常寒冷的冬天,很多公司都開始人員精簡。市場從來不缺前端,但對高階前端的需求還是特別強烈的。一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試過程中考察一些前端框架的原始碼性知識點。Vuejs
作為世界頂尖的框架之一,幾乎在所有的面試場景中或多或少都會被提及。
筆者之前在螞蟻集團就職,對於 Vue 3
的考點還是會經常問的。接下來,我將根據多年的面試以及被面試經驗,為小夥伴們梳理最近大廠愛問的 Vue 3
問題。然後我們再根據問題舉一反三,深入學習 Vue 3
原始碼知識!
要理解 Vue 3
的效能優化的核心,就需要了解 Vuejs
的核心設計理念。我們知道 Vuejs
官網上有一句話總結的特別到位:漸進式 JavaScript 框架,易學易用,效能出色,適用於場景豐富的 Web 框架。 其實我們的答案就蘊藏在這句話裡。
首先,我們知道當我們瀏覽 Web
網頁時,有兩類場景會制約 Web
網頁的效能
所以要回答這個問題,就可以直接從這兩方面入手。
對於前端框架而言,制約網路傳輸的因素最大的就是程式碼體積,程式碼體積越大,傳輸效率越慢。尤其對於 SPA
單頁應用的 CSR
(使用者端渲染) 而言。一個大體積的框架資源,就意味著使用者需要等待白屏的時間越長。而 Vue 3
在減少原始碼體積方面做的最多的就是通過精細化的 Tree-Shacking
機制來構建 漸進式
程式碼。
/*#__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
對原始碼體積的控制是多麼的用心!
在 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的瓶頸。主流瀏覽器重新整理頻率為60Hz,即每(1000ms / 60Hz)16.6ms瀏覽器重新整理一次。
我們知道,JS可以操作DOM,GUI渲染執行緒
與JS執行緒
是互斥的。所以JS指令碼執行和瀏覽器佈局、繪製不能同時執行。
在每16.6ms時間內,需要完成如下工作:
JS指令碼執行 ----- 樣式佈局 ----- 樣式繪製
當JS執行時間過長,超出了16.6ms,這次重新整理就沒有時間執行樣式佈局和樣式繪製了,也就出現了丟幀的情況,會發生卡頓。
為了解決龐大元素元件渲染、更新卡頓的問題,Vue
的策略是一方面採用了元件級的細粒度更新,控制更新的影響面:Vue 3
中,每個元件都會生成一個渲染函數,這些渲染函數執行時會進行資料存取,此時這些渲染函數被收集進入副作用函數中,建立資料 -> 副作用
的對映關係。當資料變更時,再觸發副作用函數的重新執行,即重新渲染。
另一方面則在編譯器中做了大量的靜態優化,得益於這些優化,才讓我們可以 易學易用的寫出效能出色的 Vue 專案。
下面簡單介紹幾種編譯時優化策略:
假設有以下模板:
<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
階段只做靶向更新的目的。
接下來,我們再來說一下,為什麼要做靜態提升呢? 如下模板所示:
<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
數量有限。