上一篇為了深入Vue開啟了初章,也提到createApp
和App mount
掛載的宏觀概念,今天來研究官網有蜻蜓點水著墨,但並沒有刻意放在主章節上-Vue template樣板
的本質和之前提及的渲染函數(Render Function)
。
因為章節篇幅關係,不能提及太多虛擬DOM介紹,有興趣的話看這篇文章回顧一下,是當初我還在上轉職課程的好文章。
今日學習重點:
開始今日的探索旅程~Go
在 Vue 3 開發中,我們經常使用官方建議的單文件元件(Single File Component, SFC)
作為開發單位。
SFC 是一種將 <template
、<script>
和 <style>
用標籤形式組織在一起的文件結構,這樣開發者就可以用類似 HTML、CSS 和 JavaScript 的方式來撰寫元件。透過這種結構化的方式,開發者可以更直觀地編寫和維護元件,同時也能充分利用 Vue 的特性和工具鏈來進行開發。
// Vue SFC 基本程式碼
<script setup>
import { ref } from 'vue'
const greeting = ref('Hello World!')
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>
這次我們將重點放在上次用作結尾的 Vue 渲染機制圖,並將其作為開頭的主題。
從中可以看到,SFC(單文件元件)的樣板會被轉換成渲染函式(render function)
,而這其中包含了一個編譯過程(compiler)。
如果你有興趣深入了解,可以直接進入 Vue SFC playground,你會發現雖然 SFC 的編寫看起來像是 HTML 代碼,但最終的轉換產物卻並不是 HTML 😯。
SFC 樣板的轉換過程可以概述為以下等式:
template 的最終編譯產物 = createElement (生成虛擬 DOM 節點 VNode) + 渲染函式 (render function)
樣板(Template)編譯的過程
官方文件中提到,SFC(單文件元件)文件實際上是一種特殊的格式,需要通過 @vue/compiler-sfc
編譯器來轉換成瀏覽器可以理解的 JavaScript 和 CSS。
這個編譯過程會將 .vue 文件中的 <template>
轉換為渲染函式
,並將 轉換為相應的 CSS 樣式。
編譯器裡主要有一段代碼是所謂的抽象語法樹(Abstract Syntax Tree,AST)
(圖片出處)
主要功能是將將模板轉換為一個能夠被程式語言處理的結構化數據,像是標籤種類、內容,我記得chrome瀏覽器V8引擎其中有一部分也是類似原理,就是將JS代碼轉成AST,之後後續才能進行JS運行和記憶體管理優化等。
樣板被解析轉成AST後,因為變成像JSON結構之後可以產生成為真正的Javascript Render function-渲染函式,至此這段過程是上次有提及的Vue 編譯器核心代碼(compiler core)
幫我們完成的。
渲染函式(Render Function)是什麼?
理解到樣板<template>
最終會被編譯成 JavaScript 程式碼
後,就能打破在 Vue 裡面直接撰寫 HTML 的迷思。接下來,我們可以稍微探討一下渲染函式的內容:
除了樣板部分,<script>
標籤內的程式碼會被收集到 __sfc__ 物件
中。
setup 是 Vue 3 Composition API 的語法糖,我們會在深入響應式系統時再詳細介紹。不過,當我們在 SFC 中定義事件處理函式(例如點擊事件的 function)
、ref
或 reactive
等內容時,這些都會被包含在 setup() 函式
中,而這個函式的回傳值將允許你使用 __sfc__ 物件
來調用內部的資料和方法,這樣的設計提供了更直觀的方式來組織和使用元件內的邏輯和狀態。
// SFC 文件檔編譯後所形成的JavaScript物件
const __sfc__ = {
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const msg = ref('Hello World!')
const __returned__ = { msg, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
渲染函式(Render Function)的主體:
在 Vue 中,渲染函式的主體與生成虛擬節點(VNode)的函式密切相關,例如 createElementVNode
和 createElementBlock
等,這些函式負責創建虛擬節點。虛擬節點(VNode)是虛擬 DOM 的最小單位,這些節點最終會組合成一棵完整的虛擬 DOM 樹。
嚴格來說,在 Vue 中最小的渲染單位是虛擬節點(VNode),而不是元件
。
這是因為元件在編譯後可能會由多個虛擬節點組成,形成一個更高級別的抽象物件。
因此,即使元件是 Vue 中的基本開發單位,真正參與渲染和更新的最小顆粒度應該是這些虛擬節點。
// Render Funciton 內呼叫其它創建虛擬DOM的方法
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, vModelText as _vModelText, withDirectives as _withDirectives, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString($setup.msg), 1 /* TEXT */),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, $setup.msg]
])
], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__
實際上,Vue 也為開發者提供了一個直接操作生成渲染函式的 API——h() 函式
。
h() 函式類似於 JavaScript 版的超文本標記語言(Hypertext Markup Language),是一個封裝好的函式,用於生成虛擬 DOM 節點 (createVNode())。透過 h() 函式,開發者可以直接創建虛擬節點,而不需要操作複雜的底層邏輯,也不用透過撰寫SFC文件檔來做開發。
<script setup>
// 引入 Vue 的函式
import { ref, h } from 'vue';
// 定義計數器的狀態
const count = ref(0);
// 按鈕點擊事件處理函式
const increment = () => {
count.value++;
};
// 渲染函式
const render = () =>
h('div', { class: 'counter' }, [
h('h1', '這是一個計數器範例'), // 使用 h() 生成標題節點
h('button', { onClick: increment }, `點擊我:${count.value}`), // 使用 h() 生成按鈕節點
]);
</script>
<template>
<!-- 使用 Vue 內建的渲染函式 render -->
<component :is="render" />
</template>
雖然 h() 函式提供了高靈活性,但一般開發還是推薦使用 SFC,因為它更加直觀且易於維護。
帶有編譯訊息的Virtual DOM(Compiler-Informed Virtual DOM)
恩,好奇心,因為上面編譯完的渲染函式怎麼除了createVnode
創建節點功能外,還有其它標記符號hoisting
、patch flag
?
一般傳統性Virtual DOM在需要更新比較新舊差異時,並不知道新的虛擬DOM和舊的虛擬DOM之間關係,會採用全部遍歷方式比對找出差異的部分,也因為如此即使改動一小段資料,其他保持不動的靜態樣板或資料也會將虛擬DOM一律重繪。
註解: react的一律重繪通常指的是這個部分,有點暴力性的作法但也帶來內存記憶體的消耗。
為了提升虛擬 DOM 的比較性能,Vue 3 在編譯器(compiler)中加入了幾項優化措施,使虛擬 DOM 的比對更加高效,這也是 Vue 官方推薦使用 SFC 文件模式開發的原因之一。
因為SFC 的編譯過程可以分析模板中的靜態和動態內容,在之後虛擬 DOM 比較時,能夠更精確地避免不必要的重繪,從而優化渲染性能。
簡單挑選幾項作介紹:
同樣在模板編譯過程中,靜態節點會在編譯階段被標記為 /*HOISTED*/
,這表示該虛擬節點(VNode)是靜態且不會改變的。
這些靜態節點在呼叫 createVNode
時,會被提升(hoisted)並與其他需要更新的動態節點分開處理。在之後虛擬 DOM 進行前後差異比對時,這些靜態節點會被直接略過,不參與比對,從而大幅提升渲染性能。
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, /*#__PURE__*/_toDisplayString('123'), -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, /*#__PURE__*/_toDisplayString('456'), -1 /* HOISTED */)
在樣板中帶有雙括號({{ }})
綁定的資料如果是響應性的,Vue 在編譯時除了呼叫 createVNode() 系列函數外,還會在函數後的參數中加入更新標籤(patch flag)
。
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, $setup.msg]
])
], 64 /* STABLE_FRAGMENT */))
createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)
這些標籤使用 64 位元的二進位數來表示不同的分類,具體描述哪些部分是需要更新的。例如,2 的 6 次方(即 64)表示該節點不需要更新。 (patch flag 對照表)
之後Vue的執行核心(runtime core)
,根據渲覽函式產生出來的虛擬DOM節點(v node)所帶標記(flag),後續進行虛擬DOM差異比對(differ and reconcilliation)
,最終調用執行核心和瀏覽器相關DOM操作(runtime dom)
的程式碼進行更新(patch)
。
總結:
本篇通過大量的 JavaScript 程式碼解析,打破了 Vue template 是撰寫 HTML 的迷思。
Vue 的模板在經過 @vue/compiler-sfc
編譯後,最終的產物是渲染函式(render function)
,而不是直接的 HTML。從中我們看到了 createVNode()
系列 API 如何生成虛擬節點,以及 Vue 中元件渲染的最小單位是這些虛擬DOM節點。
Vue 3 的編譯核心(compiler-core)
除了負責編譯外,還會在編譯過程中對 Vue template 中的靜態和動態節點進行分類,這樣可以在進入運行核心(runtime-core)時,提升虛擬 DOM 資料更新的計算效率。這種優化讓 Vue 能夠更精確地處理動態變化,提高整體渲染性能。
理解了開頭渲染機制圖中的渲染函式(render function)後,我們就可以開始深入探索 Vue 的響應式系統(reactive system)章節,進一步了解 Vue 如何處理數據變更與自動更新~繼續加油!
學習資源: