Virtual DOM 的實(shí)現(xiàn)原理
- 了解什么是虛擬DOM趁餐,以及虛擬DOM的作用
- Snabbdom的基本使用
- Snabbdom的源碼解析
一、什么是虛擬DOM ----Virtual DOM
-
虛擬DOM是由普通的JS對(duì)象來(lái)描述DOM對(duì)象
二篮绰、 為什么要使用Virtual DOM
- 前端開(kāi)發(fā)初期后雷,MVVM框架解決視圖和狀態(tài)同步問(wèn)題
- 模板引擎可以簡(jiǎn)化視圖操作,沒(méi)辦法跟蹤狀態(tài)
- 虛擬DOM跟蹤狀態(tài)變化
- 參考GitHub上Virtual-dom的動(dòng)機(jī)描述
- 虛擬DOM可以維護(hù)程序上的狀態(tài)吠各,跟蹤上一次的狀態(tài)
- 通過(guò)比較前后兩次狀態(tài)差異更新真實(shí)DOM
虛擬DOM用來(lái)維護(hù)視圖和狀態(tài)的關(guān)系
三臀突、虛擬DOM的作用和虛擬DOM庫(kù)
-
虛擬DOM的作用
- 維護(hù)視圖的狀態(tài)和關(guān)系
- 復(fù)雜視圖情況下提升渲染性能
- 跨平臺(tái)
- 瀏覽器平臺(tái)渲染DOM
- 服務(wù)端渲染SSR(Nuxt.js/Next.js)
- 原生應(yīng)用(Weex/React Native)
- 小程序(mpvue/uni-app)等
-
虛擬DOM庫(kù)
- Snabbdom
- Vue.js 2.x 內(nèi)部使用的虛擬DOM就是改造的Snabbdom
- 大約200 SLOC(Single line of code)
- 通過(guò)模塊可擴(kuò)展
- 源碼使用TypeScript 開(kāi)發(fā)
- 最快的Virtual Dom 之一
- virtual-dom
四、 Snabbdom 的基本使用
- 1走孽、基本步驟:
- 初始化項(xiàng)目目錄并安裝Parcel
//創(chuàng)建項(xiàng)目目錄
md snabbdom-demo
//進(jìn)入項(xiàng)目目錄
cd snabbdom-demo
//創(chuàng)建package.json
npm init -y
//本地安裝parcel
npm install parcel-bundler -D
- 配置package.json中的scripts
"scripts:"{
"dev":"parcel index.html --open",
"build":"parcel build index.html"
}
- 創(chuàng)建目錄結(jié)構(gòu)
- 根目錄創(chuàng)建index.html,引入src目錄中的文件
- 在src中創(chuàng)建js文件來(lái)導(dǎo)入使用的snabbdom進(jìn)行編碼
- 2惧辈、導(dǎo)入Snabbdom
Snabbdom 文檔:
英文:https://github.com/snabbdom/snabbdom
中文:https://github.com/coconilu/Blog/issues/152
當(dāng)前版本:v2.1.0
//導(dǎo)入snabbdom
npm install snabbdom@2.1.0
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
- 3、案例1
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// 第一個(gè)參數(shù):標(biāo)簽+選擇器
// 第二個(gè)參數(shù):如果是字符串就是標(biāo)簽中的文本內(nèi)容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// patch函數(shù)中的第一個(gè)參數(shù):舊的VNode磕瓷,也可以是DOM元素
// 第二個(gè)參數(shù):新的VNode
// 返回新的VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx','hello Snabbdom')
patch(oldVnode,vnode)
- 4盒齿、案例2
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
let vnode = h('div#container', [
h('h1', 'hello Snabbdom'),
h('p', '這是一個(gè)段落'),
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// 更改內(nèi)容
// vnode = h('div#container', [h('h1', 'hello World'), h('p', 'hello P!')])
// patch(oldVnode, vnode)
//清除div中的內(nèi)容 h('!')-->生成空的節(jié)點(diǎn)
patch(oldVnode, h('!'))
}, 2000)
五、 Snabbdom 模塊的使用
- 模塊的作用
- Snabbdom 的核心庫(kù)并不能處理DOM元素的屬性困食、樣式边翁、事件等,可以通過(guò)注冊(cè)Snabbdom默認(rèn)提供的模塊來(lái)實(shí)現(xiàn)
- Snabbdom 中的模塊可以用來(lái)擴(kuò)展Snabbdom的功能
- Snabbdom中的模塊的實(shí)現(xiàn)是通過(guò)注冊(cè)全局的鉤子來(lái)實(shí)現(xiàn)的
- 官方提供的模塊
- attributes 設(shè)置元素屬性 會(huì)處理布爾類型的屬性
- props 設(shè)置元素屬性 不會(huì)處理布爾類型的屬性
- dataset 處理html5中的data-的自定義屬性
- class 用來(lái)切換類樣式
- style 用來(lái)設(shè)置行內(nèi)樣式硕盹,可以很容易設(shè)置過(guò)度動(dòng)畫(huà)
- eventlisteners 用來(lái)注冊(cè)和移除事件
- 模塊的使用步驟
- 導(dǎo)入需要的模塊
- init()中注冊(cè)模塊
- h()函數(shù)中的第二個(gè)參數(shù)處使用模塊
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 1符匾、導(dǎo)入模塊
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
// 2、注冊(cè)模塊
const patch = init([styleModule, eventListenersModule])
// 3瘩例、使用h()函數(shù)的地兒個(gè)參數(shù)傳入模塊中使用的數(shù)據(jù)(對(duì)象)
let vnode = h('div', [
h('h1', { style: { background: 'red' } }, 'hello World'),
h('p', { on: { click: eventHandler } }, 'hello p'),
])
function eventHandler() {
console.log('點(diǎn)擊了')
}
let app = document.querySelector('#app')
patch(app, vnode)
六啊胶、 Snabbdom 源碼解析
- 如何學(xué)習(xí)源碼
- 宏觀了解
- 待著目標(biāo)看源碼
- 看源碼的過(guò)程要圍繞核心目標(biāo)
- 調(diào)試
- 參考資料
- Snabbdom的核心
- init()設(shè)置模塊甸各,創(chuàng)建patch()函數(shù)
- 使用h()函數(shù)創(chuàng)建javascript對(duì)象(VNode)描述真實(shí)DOM、
- patch()比較新舊兩個(gè)Vnode
- 把變化的內(nèi)容更新到真實(shí)的DOM樹(shù)
- 源碼地址
- 英文:https://github.com/snabbdom/snabbdom
- 中文:https://github.com/coconilu/Blog/issues/152
-當(dāng)前版本:V2.1.0
- 克隆代碼
- git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
npm install
npm run build
查看
七焰坪、 h() 函數(shù)
- h函數(shù)介紹
- 作用:創(chuàng)建vNode 對(duì)象
- vue中的h函數(shù)
- h 函數(shù)最早見(jiàn)于hyperscript,使用JavaScript創(chuàng)建超文本
//vue中的h函數(shù)
new Vue({
router,
store,
render:h => h(App)
}).$mount('#app)
- 函數(shù)重載
- 概念:參數(shù)個(gè)數(shù)或參數(shù)類型不同的函數(shù)趣倾,重載的概念和參數(shù)相關(guān),和返回值無(wú)關(guān)
- JavaScript 中沒(méi)有重載的概念
- TypeScript中有重載某饰,不過(guò)重載的實(shí)現(xiàn)還是通過(guò)代碼調(diào)整參數(shù)
//函數(shù)重載--參數(shù)個(gè)數(shù)
function add(a:number,b:number){
console.log(a+b);
}
function add(a:number,b:number,c:number){
console.log(a+b+c);
}
add(1,2)
add(1,2,3)
//函數(shù)重載--參數(shù)類型
function add(a:number,b:number){
console.log(a+b);
}
function add(a:number,b:string){
console.log(a+b);
}
add(1,2)
add(1,'2')
// 函數(shù)的重載
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}
var children: any
var text: any
var i: number
// 處理參數(shù)儒恋,實(shí)現(xiàn)重載的機(jī)制
if (c !== undefined) {
// 處理三個(gè)參數(shù)的情況
// sel data children/text
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
// 如果c是字符串或者數(shù)字
} else if (is.primitive(c)) {
text = c
// 如果c 是VNode
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
// 處理children中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果child 是string/number,創(chuàng)建文本節(jié)點(diǎn)
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 如果是svg,添加命名空間
addNS(data, children, sel)
}
// 返回VNode
return vnode(sel, data, children, text, undefined)
};
八、 快捷鍵
快速定位
Alt + ←向左方向鍵
ctrl+鼠標(biāo)左鍵
九黔漂、 VNode
十诫尽、 Patch 整體過(guò)程分析
- patch(oldVnode,newVnode)
- 把新節(jié)點(diǎn)中變化的內(nèi)容渲染到真實(shí)的DOM,最后返回新節(jié)點(diǎn)作為下一次處理的舊節(jié)點(diǎn)
- 對(duì)比新舊VNode是否相同節(jié)點(diǎn)(節(jié)點(diǎn)的key和sel相同)
- 如果不是相同節(jié)點(diǎn)炬守,刪除之前的內(nèi)容牧嫉,重新渲染
- 如果是相同節(jié)點(diǎn),再判斷新的VNode是否有text,如果有并且和oldVnode 的text不同劳较,直接更新文本內(nèi)容
- 如果有新的VNode有children驹止,判斷子節(jié)點(diǎn)是否有變化
十一、patchVnode
十二观蜗、 Diff 算法
-
虛擬DOM中的Differences算法
-
查找兩棵樹(shù)每一個(gè)節(jié)點(diǎn)的差異
-
-
Snabbdom 根據(jù)DOM的特點(diǎn)對(duì)傳統(tǒng)的diff算法做了優(yōu)化
- DOM操作時(shí)候很少會(huì)跨級(jí)別操作節(jié)點(diǎn)
-
只比較同級(jí)別的節(jié)點(diǎn)
-
對(duì)比子節(jié)點(diǎn)的具體過(guò)程,在對(duì)開(kāi)始和結(jié)束節(jié)點(diǎn)比較的時(shí)候衣洁,共有四種情況
- oldStartVnode / newStartVnode (舊開(kāi)始節(jié)點(diǎn) / 新開(kāi)始節(jié)點(diǎn))
- oldEndVnode / newEndVnode (舊結(jié)束節(jié)點(diǎn) / 新結(jié)束節(jié)點(diǎn))
- oldStartVnode / newEndVnode (舊開(kāi)始節(jié)點(diǎn) / 新結(jié)束節(jié)點(diǎn))
- oldEndVnode / newStartVnode (舊結(jié)束節(jié)點(diǎn) / 新開(kāi)始節(jié)點(diǎn))
-
開(kāi)始和結(jié)束節(jié)點(diǎn)
- 如果新舊開(kāi)始節(jié)點(diǎn)是sameVnode(key和sel相同)
- 調(diào)用patchVnode()對(duì)比和更新節(jié)點(diǎn)
-
把舊開(kāi)始和新開(kāi)始索引往后移動(dòng) oldStartIdx ++ / newStartIdx ++
- 如果新舊開(kāi)始節(jié)點(diǎn)是sameVnode(key和sel相同)
-
舊開(kāi)始節(jié)點(diǎn) / 新結(jié)束節(jié)點(diǎn)
- 調(diào)用patchVnode()對(duì)比和更新節(jié)點(diǎn)
-
把oldStartVnode對(duì)應(yīng)的DOM元素墓捻,移動(dòng)到右邊,更新索引
-
舊結(jié)束節(jié)點(diǎn) / 新開(kāi)始節(jié)點(diǎn)
- 調(diào)用patchVnode()對(duì)比和更新節(jié)點(diǎn)
-
把oldStartVnode對(duì)應(yīng)的DOM元素坊夫,移動(dòng)到左邊砖第,更新索引
-
非上述四種情況
-
循環(huán)結(jié)束
- 當(dāng)老節(jié)點(diǎn)的所有子節(jié)點(diǎn)先遍歷完(oldStartIdx>oldEndIdx),循環(huán)結(jié)束
- 當(dāng)新節(jié)點(diǎn)的所有子節(jié)點(diǎn)先遍歷完(newStartIdx>newEndIdx),循環(huán)結(jié)束
-
oldStartIdx > oldEndIdx
- 如果老節(jié)點(diǎn)的數(shù)組先遍歷完(oldStartIdx > oldEndIdx)
-
說(shuō)明新節(jié)點(diǎn)有剩余,把剩余節(jié)點(diǎn)批量插入到右邊
-
- 如果老節(jié)點(diǎn)的數(shù)組先遍歷完(oldStartIdx > oldEndIdx)
-
newStartIdx >newEndIdx
- 如果新節(jié)點(diǎn)的數(shù)組先遍歷完(newStartIdx > newEndIdx)
-
說(shuō)明老節(jié)點(diǎn)有剩余环凿,把剩余節(jié)點(diǎn)批量刪除
-
- 如果新節(jié)點(diǎn)的數(shù)組先遍歷完(newStartIdx > newEndIdx)