前言 今年又是一個非常寒冷的冬天,很多公司都開始人員精簡。市場從來不缺前端,但對高級前端的需求還是特別強烈的。一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試過程中考察一些前端框架的源碼性知識點。Vuejs 作為世界頂尖的框架之一,幾乎在所有的面試場景中或多或少都會被提及。 筆者之前 ...
前言
今年又是一個非常寒冷的冬天,很多公司都開始人員精簡。市場從來不缺前端,但對高級前端的需求還是特別強烈的。一些大廠的面試官為了區分候選人對前端領域能力的深度,經常會在面試過程中考察一些前端框架的源碼性知識點。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 技術揭秘》。
接下來的文章將繼續為大家解讀 Vue 3
響應式設計原理和非同步調度更新策略。
推廣自己的小冊
如果你對 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
插槽是如何實現內容分發的。
為方便你理解,我整理出來瞭如下的思維導圖:
相信掌握了本小冊這些模塊的核心原理之後,你再去閱讀 Vue 3 源碼或者是解決 Vue 3 的疑難雜症時,會更加得心應手。