前言
最近在深入學習 Vue.js設計與實現 ,深感“理解設計理念”比“記憶API用法”更為重要。
我決定通過文章來記錄和輸出學習內容,這不僅是為了檢驗自己的理解程度,更是為了梳理Vue.js背後的核心思想和實現機制。希望這種“知其所以然”的探索,能為你帶來啓發。
1.1命令式和聲明式
首先什麼是聲明式和命令式,看接下來我們要做的事
獲取 id 為 app 的 div 標籤
它的文本內容為 hello world
為其綁定點擊事件
當點擊時彈出提示:ok
命令式代碼:你如何去做,自己手動操作DOM,核心概念就是 關注過程
const div = document.querySelector('#app') // 獲取 div
div.innerText = 'hello world' // 設置文本內容
div.addEventListener('click', () => { alert('ok') }) // 綁定點擊事件
聲明式代碼:你描述想要的結果,Vue自動幫你實現DOM操作,核心概念就是 關注結果
<div @click="() => alert('ok')">hello world</div>
更通俗一點的説法:
- 命令式代碼是廚師,你需要洗菜、切菜、炒菜,每個步驟都需要你來
- 聲明式代碼是顧客,我只需要跟你説一聲我要吃什麼,接下來就交給你了
1.2 性能和可維護性的權衡
聲明式代碼好是好,但他會有更多的性能消耗,藉助書中的一句結論:聲明式代碼的性能不優於命令式代碼的性能
看上面的例子 要求:把div中的文本要求改成 hzh 真帥。
命令式代碼:
div.textContent = 'hzh 真帥'
聲明式代碼:
<div @click="handleClick">{{ message }}</div>
const app = {
data() {
return {
message: 'hello world'
}
},
methods: {
handleClick() {
// 聲明式更新:只改變數據,Vue自動更新DOM
this.message = 'hzh 真帥';
}
}
}
//當我們的this.message = 'hzh 真帥'的時候,vue自動做了這麼一步
div.textContent = 'hzh 真帥'
書中結論:
如果我們把直接修改的性能消耗定義為 A,把找出差異的性能消耗定義為 B,那麼有: <br/>
● 命令式代碼的更新性能消耗 = A <br/>
● 聲明式代碼的更新性能消耗 = B + A
那既然聲明式代碼會消耗更多的性能,那我們是不是要更多的使用命令式代碼?答案是不一定,直接上截圖
其實對於大多數業務應用,聲明式的可維護性優勢遠大於其性能開銷。而且隨着框架優化技術的進步,這個性能差距正在不斷縮小。這也是為什麼vue,react這些聲明式框架會成為主流
1.3 虛擬DOM的性能到底如何
首先,虛擬DOM是什麼?先看看真實DOM是什麼
真實DOM
// 真實DOM - 瀏覽器中的實際節點
<div class="title" id="app">Hello World</div>
虛擬DOM
// 對應的虛擬DOM
const vnode = {
type: 'div',
props: {
class: 'title',
id: 'app'
},
children: 'Hello World'
}
可以看出,虛擬DOM就是一個js對象,為什麼要有虛擬DOM這個東西呢,回想之前的聲明式代碼,把找出差異的性能消耗定義為 B,虛擬DOM就是為了優化這個'找出差異(B)'的性能消耗(書中原話:虛擬DOM就是為了最小化找到差異這一步性能消耗而出現的)
DOM的更新方式以下有幾種
- 原生的js操作
- innerHTML
- 虛擬DOM
innerHTML更新需要觸發需要刪除以前的元素,在重新更新新的元素
innerHTML創建頁面的性能:HTML 字符串拼接的計算量 + innerHTML 的 DOM 計算量
<div id="list">
<li>項目1</li>
<li>項目2</li>
</div>
// 更新列表 - 重新渲染整個列表
const list = document.getElementById('list');
list.innerHTML = `
<li>項目1</li>
<li>項目2</li>
<li>新項目3</li> <!-- 只增加了這一項 -->
`;
虛擬DOM創建js對象,這個對象可以理解成真實DOM,並使用遞歸地遍歷虛擬 DOM 樹並創建真實 DOM
虛擬DOM創建頁面的性能:創建 JavaScript 對象的計算量 + 創建真實 DOM 的計算量
// 舊的虛擬DOM
const oldVNodes = [
{ type: 'li', children: '項目1' },
{ type: 'li', children: '項目2' }
];
// 新的虛擬DOM
const newVNodes = [
{ type: 'li', children: '項目1' },
{ type: 'li', children: '項目2' },
{ type: 'li', children: '新項目3' }
];
那麼他兩的區別在哪呢?
innerHTML的更新過程:innerHTML的更新過程會銷燬所有現有DOM元素,然後重新創建新的DOM元素
大概意思是:
原本:list.innerHTML = `
<li>項目1</li>
<li>項目2</li>
`;
->
銷燬之後:list.innerHTML = ``
->
重新更新:list.innerHTML = `
<li>項目1</li>
<li>項目2</li>
<li>新項目3</li>
`;
虛擬DOM的更新過程:javaScript對象+diff,只會更新更改的元素
// 虛擬DOM如何工作
const oldVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'Item 1' },
{ type: 'li', children: 'Item 2' }
]
};
const newVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'Item 1' },
{ type: 'li', children: 'Updated Item 2' }, // 只有這裏變了
{ type: 'li', children: 'Item 3' } // 新增
]
};
// Diff算法發現:
// - 第一個li沒變,複用
// - 第二個li文本變了,只更新文本
// - 新增第三個li,創建新元素
虛擬DOM的優勢在於:無論頁面有多大,我只更新變化的地方,而innerHTML則是全部銷燬更新,這對性能消耗很大
書中原話:innerHTML、虛擬 DOM 以及原生 JavaScript 在更新頁面時的性能我們分了幾個維度:心智負擔、可維護性和性能
1.4編譯時和運行時
當設計一個框架的時候,我們有三種選擇:純運行時的、運行時 +編譯時的或純編譯時的
運行時
假設一個框架,提供一個render函數,我們可以提供一個樹型結構的對象
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
tag代表標籤名稱,children即可以是一個數組(代表子元素),也可以是一段文本內容,接下來實現一下render函數
function Render(obj, root) {
const el = document.createElement(obj.tag)
if (typeof obj.children === 'string') {
const text = document.createTextNode(obj.children)
el.appendChild(text)
} else if (obj.children) {
// 數組,遞歸調用 Render,使用 el 作為 root 參數
obj.children.forEach((child) => Render(child, el))
}
// 將元素添加到 root
root.appendChild(el)
}
有了這個函數之後,我們可以這樣使用來渲染到body下
Render(obj, document.body)
編譯時
有時我們覺得,直接寫一個樹型結構的對象好麻煩啊,我能不能獲取一下html標籤把他變成樹型結構的對象呢?,為此寫了一個Compiler函數,可以實現上述效果
const html = `
<div>
<span>hello world</span>
</div>
`
// 調用 Compiler 編譯得到樹型結構的數據對象
const obj = Compiler(html)
Compiler函數做的事:將html編譯成命令式代碼的過程
那麼這三種選擇分別有哪些優缺點呢?
運行時:
- ✅ 無構建步驟:直接引入即可使用
- ✅ 靈活性高:可以在運行時動態創建任何結構
- ✅ 調試簡單:沒有編譯過程,代碼就是最終代碼
- ❌ 性能較差:無法進行編譯時優化
- ❌ 代碼冗長:需要手動創建DOM和綁定事件
- ❌ 無語法糖:沒有模板、JSX等便捷語法
編譯時:
- ✅ 性能極佳:編譯時完成所有優化,運行時幾乎零開銷
- ✅ 包體積小:不需要包含運行時框架代碼
- ✅ 無虛擬DOM:直接操作DOM,減少內存佔用
- ❌ 靈活性差:難以在運行時動態創建複雜結構
- ❌ 調試困難:編譯後代碼與源代碼差異大
- ❌ 構建依賴:必須使用構建工具,無法直接運行
編譯時 + 運行時:
- ✅ 開發體驗好:提供模板、JSX等友好語法
- ✅ 性能優化:編譯時可以進行靜態分析優化
- ✅ 靈活性:支持運行時動態創建
- ✅ 漸進式:可以選擇使用編譯特性
- ⚠️ 需要構建工具:需要配置webpack、vite等
- ⚠️ 包體積較大:包含編譯器和運行時
- ⚠️ 複雜度高:需要處理編譯和運行時的協調
總結
通過對比命令式與聲明式、分析虛擬DOM原理、瞭解編譯時與運行時的選擇,我們看到了Vue.js在性能與可維護性之間的智慧平衡。這種"知其所以然"的理解,能讓我們更好地使用和欣賞這個優秀的框架。
希望這篇文章對你有所啓發!