Vue原理
組件化和MVVM
數(shù)據(jù)驅動視圖
- 傳統(tǒng)組件,只是靜態(tài)渲染,更新還要依賴操作DOM
- 數(shù)據(jù)驅動視圖 - vue MVVM
- 數(shù)據(jù)驅動視圖 - react setState() useState();
MVVM

- Model(模型)
- Model 代表應用的數(shù)據(jù)和業(yè)務邏輯。它負責存儲和管理應用的狀態(tài)。Model 可以包含從服務器獲取的數(shù)據(jù),或應用程序中需要操作的數(shù)據(jù)
- Model 不關心 UI,它只處理數(shù)據(jù)和與數(shù)據(jù)相關的操作
- View(視圖)
- View 是應用的 UI 部分,負責顯示數(shù)據(jù)(即 UI 展現(xiàn))。它通常是由 HTML、CSS 等組成的界面。View 關注的是如何呈現(xiàn)數(shù)據(jù)。
- View 被動地顯示數(shù)據(jù)變化,但它不能直接修改數(shù)據(jù)
- ViewModel(視圖模型)
- ViewModel 是 View 和 Model 之間的橋梁,它負責將 Model 中的數(shù)據(jù)處理為 View 所需的格式,并且處理用戶交互(如點擊、輸入等)。ViewModel 監(jiān)聽用戶對 View 的操作,并更新 Model 數(shù)據(jù),然后再將更新后的數(shù)據(jù)展示給 View
- ViewModel 不直接操作 DOM,而是通過數(shù)據(jù)綁定(如 Vue 中的雙向數(shù)據(jù)綁定)來驅動 UI 的更新
響應式原理
核心API
- Vue 2 主要采用 Object.defineProperty() 實現(xiàn)數(shù)據(jù)的劫持和依賴收集。
- Vue 3 使用 Proxy 替代了 Object.defineProperty()
響應式的核心機制
Vue2.0通過數(shù)據(jù)劫持(數(shù)據(jù)監(jiān)聽)+ 發(fā)布 - 訂閱模式來實現(xiàn)響應式
- Observer(數(shù)據(jù)監(jiān)聽): 使用Object.defineProperty()劫持對象的屬性,并在屬性被讀取或修改時觸發(fā)回調(diào)
- Dep(依賴收集):用于管理依賴(訂閱者),當數(shù)據(jù)變更,通知所有依賴更新
- Watcher(觀察者): 作為Vue組件與數(shù)據(jù)的橋梁,當庭數(shù)據(jù)變化后,觸發(fā)視圖更新
響應式流程
- 數(shù)據(jù)初始化
- 依賴收集
- 視圖更新
Object.definedProperty 缺點
- 深度監(jiān)聽,需要遞歸到底,一次性計算量大
- 無法監(jiān)聽新增刷新/刪除屬性(Vue.set Vue.delete)
- Vue.set(obj, "name", "value");
- Vue.delete(obj, key)
- 無法監(jiān)聽原生數(shù)組,通過重寫數(shù)組方法(如 push、pop、splice)來監(jiān)聽數(shù)組變更,但不能監(jiān)聽數(shù)組索引的直接修改
vDom和diff
- vdom是實現(xiàn)vue和react的重要基石
- diff算法是vdom中最核心、最關鍵的部分
虛擬DOM(Virtual DOM)
Virtual DOM(虛擬 DOM) 是 Vue、React 等前端框架中用于優(yōu)化 DOM 操作的一種技術。它的核心思想是用 JavaScript 對象模擬真實 DOM 結構,然后在數(shù)據(jù)變化時對比新舊 VDOM 生成最小的更新補丁,再應用到真實 DOM,從而提高渲染性能。
為什么要有vdom
- 頻繁操作DOM開銷大,每次操作都需要觸發(fā)回流和重繪
- 數(shù)據(jù)驅動UI復雜: 手動管理DOM變化需要寫大量的代碼,容易出錯,難為維護
優(yōu)勢
- 減少直接DOM操作,在js中先計算變更,再一次性更新DOM
- 優(yōu)化渲染性能:使用diff算法計算最小的dom更新,提高頁面性能
- 跨平臺能力,vdom不依賴瀏覽器,react native等移動端框架也基于vdom構建
VDOM的實現(xiàn)
Vue2參考的是Snabbdom作為Virtual DOM庫,它的實現(xiàn)主要包括:
- VNode(虛擬節(jié)點):用javascript對象表示DOM結構
相當于const vnode = { tag: "div", data: { id: "app", class: "container" }, children: [ { tag: "p", data: {}, text: "Hello Virtual DOM" } ] };<div id="app" class="container"> <p>Hello Virtual DOM</p> </div> - Diff算法: 新舊VNode進行對比,計算最小修改
- 同級比較:只比較同一層的節(jié)點,不進行夸層對比,減少計算量
- Key機制: 通過key標識節(jié)點,提高Diff速度
- 雙端對比(雙指針優(yōu)化):減少無用遍歷,優(yōu)化列表更新
- 四種節(jié)點變更情況:
- 新增節(jié)點 -> 直接插入
- 刪除節(jié)點 -> 直接移除
- 修改節(jié)點 -> 更新屬性、文本
- 移動節(jié)點 -> 調(diào)整順序,減少dom操作
- Patch過程:把差異更新到真實DOM
通過snabbdom學習VDOM
- h函數(shù)
- vdom數(shù)據(jù)結構
- patch函數(shù)
VDOM總結
- 用js模擬DOM結構
- 新舊vnode對比,得出最小的更新范圍,最后更新DOM
- 數(shù)據(jù)驅動視圖的模式下,有效控制DOM操作
diff算法
概覽
Diff(差異對比算法)用于比較新舊 Virtual DOM(VNode),找出最小變化,并將變更高效地應用到真實 DOM。
- 新舊VNode是否是同一個節(jié)點
- 對比屬性和文本內(nèi)容
- 對比子節(jié)點(雙端對比 + key機制優(yōu)化)
背景
- 樹diff的時間 復雜度O(n^3)
- 遍歷tree1
- 遍歷tree2
- 排序
- 優(yōu)化時間復雜度到O(n)
- 只比較統(tǒng)一層級,不跨級比較
- tag不相同,則直接刪除重建,不再深度比較
- tag和key,兩者都相同,則認為是相同節(jié)點,不再深度比較
diff算法詳細過程
- 判斷是否是同一節(jié)點
- 相同:直接對比屬性&子節(jié)點
- 不相同:直接刪除舊節(jié)點,創(chuàng)建新節(jié)點
- 判斷規(guī)則:
- 標簽相同
- key相同
- 都是文本節(jié)點
const oldVNode = { tag: "p", text: "Hello" }; const newVNode = { tag: "p", text: "Hello Vue" }; // 只修改文本,不刪除 p 標簽 patch(oldVNode, newVNode);
- 對比屬性和文本
- 屬性不同 - 更新屬性
- 文本不同 - 更新textContent
最終更新:// 舊 VNode const oldVNode = { tag: "p", data: { class: "text" }, text: "Hello" }; // 新 VNode const newVNode = { tag: "p", data: { class: "text updated" }, text: "Hello Vue" }; // Vue Patch 過程: // 1. class 變更 → 僅修改 class // 2. 文本變更 → 僅修改 textContent patch(oldVNode, newVNode);<p class="text">Hello</p> ↓ <p class="text updated">Hello Vue</p> - 對比子節(jié)點
如果節(jié)點相同,vue遞歸比較chidren:- 舊節(jié)點有,新節(jié)點沒有 -> 刪除舊節(jié)點
- 新節(jié)點有,舊節(jié)點沒有 -> 直接新增
- 新舊都有 -> 進行雙端對比+key機制優(yōu)化
- 雙端diff優(yōu)化
- 從頭開始比較(左側)
- 從尾部開始比較(右側)
- 移動節(jié)點:使用key識別相同節(jié)點,避免刪除+重建
vdom和diff算法總結
- patchVNode
- addVNodes removeVNodes
- updateChildren(key的重要性)
- 細節(jié)不重要,updateChildren的過程不重要,不要深究
- vdom核心概念很重要:h、vnode、patch、diff、key
- vdom存在的價值更加重要:數(shù)據(jù)驅動視圖,控制DOM
LIS算法
在 Vue 2 的 Diff 過程中,列表比較采用雙端比較(雙指針優(yōu)化),但如果有大量元素位置變化,Vue 2 可能會執(zhí)行多次插入和刪除,導致不必要的 DOM 操作。
Vue 3 進一步優(yōu)化了 Diff 算法,引入了 LIS(最長遞增子序列,Longest Increasing Subsequence),減少 DOM 移動次數(shù),使列表更新更高效。
LIS 是指在一個數(shù)組中,找到最長的遞增子序列(不要求連續(xù)), Vue 3 的 Diff 算法中,LIS 用來找到可以復用的元素,避免不必要的移動。
輸入: [10, 9, 2, 5, 3, 7, 101, 18]
最長遞增子序列: [2, 3, 7, 18](長度 4)
vue3Diff過程
- 從前往后找到相同前綴(頭部相同不變)
- 從后往前找到相同后綴(尾部相同不變)
- 對比中間不同部分
- 查找最長遞增子序列
- 只移動非LIS部分
- LIS部分直接復用,避免多余操作
vue2和vu3 diff更新對比
<!-- 舊列表 -->
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
<li key="d">D</li>
</ul>
<!-- 新列表(調(diào)整順序) -->
<ul>
<li key="a">A</li>
<li key="c">C</li>
<li key="d">D</li>
<li key="b">B</li>
</ul>
- vue2處理方式:
- 發(fā)現(xiàn) A 沒變
- 發(fā)現(xiàn) B 位置變了(誤認為B被刪除,再新增到最后)
- C 和 D 位置不變
- 最終:刪除B,創(chuàng)建B,導致額外的dom操作
- vue3處理方式
- A 位置不變
- C->D 不一定能夠
- B(需要調(diào)整)
- 最終:只移動B,而不是先刪除+新增
模版編譯
js的with語法
with 語句用于擴展作用域鏈,讓代碼在一個對象的作用域內(nèi)執(zhí)行,避免重復書寫對象名。
const person = {
name: "Alice",
age: 25,
job: "Engineer"
};
with (person) {
console.log(name); // 直接訪問 person.name
console.log(age); // 直接訪問 person.age
console.log(job); // 直接訪問 person.job
}
with 會影響作用域鏈的解析,導致代碼難以閱讀和調(diào)試,因此被認為是不安全的,在嚴格模式(strict mode)下被禁止。
vue template complier將模版編譯為render函數(shù)
具體代碼見vue原理 - 模版編譯 - vue-template-compiler-demo
背景
- 模版不是html,有指令、插值、js表達式、能實現(xiàn)判斷、循環(huán)
- html是標簽語言,只有js才能實現(xiàn)判斷、虛幻(圖靈完備的)
- 因此,模版一定是轉換為某種js代碼,即編譯模版
過程
- 解析,將vue模版解析為AST(抽象語法樹),即一個樹型結構數(shù)據(jù)表示
- 轉換,在ast的基礎上進行轉換處理
- 指令解析(v-if, v-for)
- 表達式解析({{message}})
- 靜態(tài)優(yōu)化(標記靜態(tài)節(jié)點,提高更新性能)
- 代碼生成,將AST轉換為可執(zhí)行的函數(shù)(render function),即h函數(shù)(vNode)
- 最終 render() 函數(shù)在組件渲染時執(zhí)行,生成 Virtual DOM(虛擬 DOM),再通過 Diff 算法 更新真實 DOM。
總結
- 模版編譯為render函數(shù),執(zhí)行render函數(shù)會返回vnode
- 基于vnode再執(zhí)行patch和diff
- 使用webpack vue-loader 會在開發(fā)環(huán)境下編譯模版(重要),屬于優(yōu)化手段
執(zhí)行render函數(shù)生成vnode
vue組件中使用render代替了template
react一直都用render
組件更新渲染過程
組件是異步渲染的,匯總數(shù)據(jù)data修改,一次性更新
[圖片上傳失敗...(image-441047-1741339501073)]
- 初次渲染過程
- 創(chuàng)建vue實例
- 解析template為render函數(shù)
- 觸發(fā)響應式,監(jiān)聽data屬性的getter 和 setter
- 執(zhí)行render函數(shù),生成vNode
- patch(elem, newVNode),將vNode轉換為真實的DOM并掛載到頁面上:
- 更新過程
- 修改data,觸發(fā)setter(此前在getter中已被監(jiān)聽)
- 重新生成render函數(shù),生成新的vNode
- patch(oldVNode, newVNode),通過diff,找到變更的節(jié)點,渲染到頁面上
前端路由
前端路由的原理
前端路由的本質是在不刷新頁面的情況下,更新URL并改變頁面內(nèi)容,他主要依賴History API 或 Hash模式來監(jiān)聽URL變化,并根據(jù)不同的url渲染組件
原因
- 傳統(tǒng)的后端路由方式下,每次訪問新的頁面都會向服務器請求新的html,每次頁面跳轉都會重新加載,影響用戶體驗
- 前端路由讓頁面切換更快:
- 避免整頁刷新
- 只更新部分視圖
- 更適合單頁面應用
前端路由的兩種模式
hash路由
- hash變化會觸發(fā)網(wǎng)頁跳轉,即瀏覽器的前進,后退
- hash變化不會刷新頁面,spa必需的特點
- hash永遠不會提交到server端
history路由
- 用url規(guī)范的路由,但跳轉是不刷新頁面
- history.pushState
- window.onpopstate
- 需要服務端支持
如何選擇
- to b系統(tǒng)推薦用hash,簡單易用,對url規(guī)范不敏感
- to c系統(tǒng),可以考慮h5 history,但需要服務端支持
- 能選擇簡單的,就別用復雜的,要考慮成本和收益