[TOC]
<br>
<br>
# 模板轉換成視圖的過程
* Vue.js通過編譯將template 模板轉換成渲染函數(render ) ,執行渲染函數就可以得到一個虛擬節點樹
* 在對 Model 進行操作的時候,會觸發對應 Dep 中的 Watcher 對象。Watcher 對象會調用對應的 update 來修改視圖。這個過程主要是將新舊虛擬節點進行差異對比,然后根據對比結果進行DOM操作來更新視圖。
<br>
簡單點講,在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在狀態改變時,Vue能夠智能地計算出重新渲染組件的最小代價并應到DOM操作上。
<br>

<br>
我們先對上圖幾個概念加以解釋:
* **渲染函數**:渲染函數是用來生成Virtual DOM的。Vue推薦使用模板來構建我們的應用界面,在底層實現中Vue會將模板編譯成渲染函數,當然我們也可以不寫模板,直接寫渲染函數,以獲得更好的控制。
* **VNode 虛擬節點**:它可以代表一個真實的 dom 節點。通過 createElement 方法能將 VNode 渲染成 dom 節點。簡單地說,vnode可以理解成**節點描述對象**,它描述了應該怎樣去創建真實的DOM節點。
* **patch(也叫做patching算法)**:虛擬DOM最核心的部分,它可以將vnode渲染成真實的DOM,這個過程是對比新舊虛擬節點之間有哪些不同,然后根據對比結果找出需要更新的的節點進行更新。這點我們從單詞含義就可以看出, patch本身就有補丁、修補的意思,其實際作用是在現有DOM上進行修改來實現更新視圖的目的。Vue的Virtual DOM Patching算法是基于[Snabbdom](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsnabbdom%2Fsnabbdom "https://github.com/snabbdom/snabbdom")的實現,并在些基礎上作了很多的調整和改進。
<br>
<br>
# Virtual DOM
## Virtual DOM 是什么?
Virtual DOM 其實就是一棵以 JavaScript 對象( VNode 節點)作為基礎的樹,用對象屬性來描述節點,實際上它只是一層對真實 DOM 的抽象。最終可以通過一系列操作使這棵樹映射到真實環境上。
<br>
簡單來說,可以把Virtual DOM 理解為一個簡單的JS對象,并且最少包含標簽名( tag)、屬性(attrs)和子元素對象( children)三個屬性。不同的框架對這三個屬性的命名會有點差別。
<br>
對于虛擬DOM,咱們來看一個簡單的實例,就是下圖所示的這個,詳細的闡述了`模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM`的一個過程

<br>
<br>
## Virtual DOM 作用是什么?
**虛擬DOM的最終目標是將虛擬節點渲染到視圖上**。但是如果直接使用虛擬節點覆蓋舊節點的話,會有很多不必要的DOM操作。例如,一個ul標簽下很多個li標簽,其中只有一個li有變化,這種情況下如果使用新的ul去替代舊的ul,因為這些不必要的DOM操作而造成了性能上的浪費。
<br>
為了避免不必要的DOM操作,虛擬DOM在虛擬節點映射到視圖的過程中,將虛擬節點與上一次渲染視圖所使用的舊虛擬節點(oldVnode)做對比,找出真正需要更新的節點來進行DOM操作,從而避免操作其他無需改動的DOM。
<br>
其實虛擬DOM在Vue.js主要做了兩件事:
* 提供與真實DOM節點所對應的虛擬節點vnode
* 將虛擬節點vnode和舊虛擬節點oldVnode進行對比,然后更新視圖
<br>
<br>
## 為何需要Virtual DOM?
* 具備跨平臺的優勢
由于 Virtual DOM 是以 JavaScript 對象為基礎而不依賴真實平臺環境,所以使它具有了跨平臺的能力,比如說瀏覽器平臺、Weex、Node 等。
<br>
* 操作 DOM 慢,js運行效率高。我們可以將DOM對比操作放在JS層,提高效率。
因為DOM操作的執行速度遠不如Javascript的運算速度快,因此,把大量的DOM操作搬運到Javascript中,運用patching算法來計算出真正需要更新的節點,最大限度地減少DOM操作,從而顯著提高性能。
<br>
Virtual DOM 本質上就是在 JS 和 DOM 之間做了一個緩存。可以類比 CPU 和硬盤,既然硬盤這么慢,我們就在它們之間加個緩存:既然 DOM 這么慢,我們就在它們 JS 和 DOM 之間加個緩存。CPU(JS)只操作內存(Virtual DOM),最后的時候再把變更寫入硬盤(DOM)
<br>
* 提升渲染性能
Virtual DOM的優勢不在于單次的操作,而是在大量、頻繁的數據更新下,能夠對視圖進行合理、高效的更新。
為了實現高效的DOM操作,一套高效的虛擬DOM diff算法顯得很有必要。**我們通過patch 的核心----diff 算法,找出本次DOM需要更新的節點來更新,其他的不更新**。那diff 算法的實現過程是怎樣的?
<br>
## 實現 vnode
**(1)如何用 `JS` 對象模擬 `DOM` 樹**
例如一個真實的 `DOM` 節點如下:
~~~
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
復制代碼
~~~
我們用 `JavaScript` 對象來表示 `DOM` 節點,使用對象的屬性記錄節點的類型、屬性、子節點等。
`element.js` 中表示節點對象代碼如下:
~~~
/**
* Element virdual-dom 對象定義
* @param {String} tagName - dom 元素名稱
* @param {Object} props - dom 屬性
* @param {Array<Element|String>} - 子節點
*/
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
// dom 元素的 key 值,用作唯一標識符
if(props.key){
this.key = props.key
}
var count = 0
children.forEach(function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
// 子元素個數
this.count = count
}
function createElement(tagName, props, children){
return new Element(tagName, props, children);
}
module.exports = createElement;
~~~
根據 `element` 對象的設定,則上面的 `DOM` 結構就可以簡單表示為:
~~~
var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
~~~
現在 `ul` 就是我們用 `JavaScript` 對象表示的 `DOM` 結構,我們輸出查看 `ul` 對應的數據結構如下:

**(2)渲染用 `JS` 表示的 `DOM` 對象**
但是頁面上并沒有這個結構,下一步我們介紹如何將 `ul` 渲染成頁面上真實的 `DOM` 結構,相關渲染函數如下:
~~~
/**
* render 將virdual-dom 對象渲染為實際 DOM 元素
*/
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
// 設置節點的DOM屬性
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子節點也是虛擬DOM,遞歸構建DOM節點
: document.createTextNode(child) // 如果字符串,只構建文本節點
el.appendChild(childEl)
})
return el
}
~~~
我們通過查看以上 `render` 方法,會根據 `tagName` 構建一個真正的 `DOM` 節點,然后設置這個節點的屬性,最后遞歸地把自己的子節點也構建起來。
我們將構建好的 `DOM` 結構添加到頁面 `body` 上面,如下:
~~~
ulRoot = ul.render();
document.body.appendChild(ulRoot);
復制代碼
~~~
這樣,頁面 `body` 里面就有真正的 `DOM` 結構,效果如下圖所示:

<br>
<br>
# diff 算法

Vue的diff算法是基于snabbdom改造過來的,**僅在同級的vnode間做diff,遞歸地進行同級vnode的diff,最終實現整個DOM樹的更新**。因為跨層級的操作是非常少的,忽略不計,這樣時間復雜度就從O(n3)變成O(n)。
<br>
diff 算法包括幾個步驟:
* 用 JavaScript 對象結構表示 DOM 樹的結構;然后用這個樹構建一個真正的 DOM 樹,插到文檔當中
* 當狀態變更的時候,重新構造一棵新的對象樹。然后用新的樹和舊的樹進行比較,記錄兩棵樹差異
* 把所記錄的差異應用到所構建的真正的DOM樹上,視圖就更新了

<br>
## diff 過程
<br>

<br>
首先,在新老兩個VNode節點的左右頭尾兩側都有一個變量標記,在遍歷過程中這幾個變量都會向中間靠攏。當oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時結束循環。
<br>
索引與VNode節點的對應關系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode
<br>
在遍歷中,如果存在key,并且滿足sameVnode,會將該DOM節點進行復用,否則則會創建一個新的DOM節點。
<br>
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2\*2=4種比較方法。
<br>
當新老VNode節點的start或者end滿足sameVnode時,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節點進行patchVnode即可。
<br>

<br>
如果oldStartVnode與newEndVnode滿足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
<br>
這時候說明oldStartVnode已經跑到了oldEndVnode后面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的后面。
<br>

<br>
如果oldEndVnode與newStartVnode滿足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
這說明oldEndVnode跑到了oldStartVnode的前面,進行patchVnode的同時真實的DOM節點移動到了oldStartVnode的前面。

<br>
如果以上情況均不符合,則通過createKeyToOldIdx會得到一個oldKeyToIdx,里面存放了一個key為舊的VNode,value為對應index序列的哈希表。從這個哈希表中可以找到是否有與newStartVnode一致key的舊的VNode節點,如果同時滿足sameVnode,patchVnode的同時會將這個真實DOM(elmToMove)移動到oldStartVnode對應的真實DOM的前面。

<br>
當然也有可能newStartVnode在舊的VNode節點找不到一致的key,或者是即便key相同卻不是sameVnode,這個時候會調用createElm創建一個新的DOM節點。

<br>
到這里循環已經結束了,那么剩下我們還需要處理多余或者不夠的真實DOM節點。
<br>
1.當結束時oldStartIdx > oldEndIdx,這個時候老的VNode節點已經遍歷完了,但是新的節點還沒有。說明了新的VNode節點實際上比老的VNode節點多,也就是比真實DOM多,需要將剩下的(也就是新增的)VNode節點插入到真實DOM節點中去,此時調用addVnodes(批量調用createElm的接口將這些節點加入到真實DOM中去)。

<br>
2。同理,當newStartIdx > newEndIdx時,新的VNode節點已經遍歷完了,但是老的節點還有剩余,說明真實DOM節點多余了,需要從文檔中刪除,這時候調用removeVnodes將這些多余的真實DOM刪除。

<br>
<br>
# patch
patch將新老VNode節點進行比對,然后將根據兩者的比較結果進行最小單位地修改視圖,而不是將整個視圖根據新的VNode重繪。patch的核心在于diff算法,這套算法可以高效地比較viturl dom的變更,得出變化以修改視圖。
<br>
那么patch如何工作的呢?
<br>
首先說一下patch的核心diff算法,diff算法是通過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間復雜度只有O(n),是一種相當高效的算法。
<br>

<br>

<br>
著兩張圖代表舊的VNode與新VNode進行patch的過程,他們只是在同層級的VNode之間進行比較得到變化(第二張圖中相同顏色的方塊代表互相進行比較的VNode節點),然后修改變化的視圖,所以十分高效。
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼