這次我們的學(xué)習(xí)目標(biāo)有三:
1.了解什么是虛擬DOM,以及虛擬DOM的作用
2.Snabbdom的基本使用
3.Snabbdom的源碼解析
什么是Virtual DOM
- Virtual DOM就是虛擬DOM另玖,是由普通的JS對(duì)象來(lái)描述DOM對(duì)象
-
使用Virtual DOM來(lái)描述真實(shí)的DOM
為什么要使用Virtual DOM困曙?
- DOM的操作本身是性能會(huì)出現(xiàn)問(wèn)題表伦,操作比較復(fù)雜的
- MVVM框架解決視圖和狀態(tài)同步問(wèn)題
- 模板引擎可以簡(jiǎn)化視圖操作,沒(méi)辦法跟蹤狀態(tài)
(無(wú)法得知當(dāng)前頁(yè)面變化之前的狀態(tài)) - 虛擬DOM能夠跟蹤狀態(tài)變化
- 參考github上virtual-dom的動(dòng)機(jī)描述
- 虛擬DOM可以維護(hù)程序的狀態(tài)慷丽,跟蹤上一次的狀態(tài)
- 通過(guò)比較前后兩次狀態(tài)差異更新真實(shí)DOM
實(shí)際案例
傳統(tǒng)DOM操作方式:
虛擬DOM操作方式:
通過(guò)實(shí)例我們就可以輕易區(qū)分出兩者的不同之處
Virtual 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
- 通過(guò)模塊可拓展
- 源碼使用TS開(kāi)發(fā)
- 最快的Virtual DOM之一
- virtual-dom
Snabbdom基本使用方式
- 安裝parcel
- 配置scripts
-
目錄結(jié)構(gòu)
Snabbdom 文檔
當(dāng)前版本:v2.1.0
官方文檔中文翻譯:
根據(jù)文檔所述:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
console.log(init)
console.log(h)
我們引入兩個(gè)核心功能翔怎,這里要注意一下,因?yàn)閣ebpack版本問(wèn)題杨耙,我們按照官方文檔那樣引入的話會(huì)出現(xiàn)加載錯(cuò)誤的問(wèn)題赤套,所以我們應(yīng)該按照如上代碼依次查詢路徑導(dǎo)入
打印一下init和h方法,可以看到我們主要使用的方法的具體內(nèi)容
基本使用:
- 主要用到了init函數(shù)和h函數(shù)
- h函數(shù)有兩個(gè)參數(shù)珊膜,第一個(gè)參數(shù)是新定義的標(biāo)簽(包含class和id)容握,第二個(gè)參數(shù)是新的內(nèi)容(傳入字符串)
- 要獲取掛載的元素,通過(guò)init函數(shù)得到patch函數(shù)车柠,第一次聲明patch剔氏,init內(nèi)部得是空數(shù)組
- patch有兩個(gè)參數(shù),第一個(gè)是掛載的元素dom竹祷,第二個(gè)是通過(guò)h函數(shù)創(chuàng)建的vnode
詳細(xì)來(lái)看就是這樣的:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1.通過(guò)h函數(shù)創(chuàng)建VNode
let vNode = h('div#box.container', '新內(nèi)容')
// 獲取掛載元素
const dom = document.querySelector('#app')
// 2.通過(guò)init函數(shù)得到patch函數(shù)
const patch = init([])
// 3.通過(guò)patch將VNode渲染到DOM
let oldVnode = patch(dom, vNode)
// 4.創(chuàng)建新的Vnode谈跛,更新給oldVnode
vNode = h('p#text.abc', '這是p標(biāo)簽')
patch(oldVnode, vNode)
包含子節(jié)點(diǎn)
跟基本使用大體上是一樣的,但是有一點(diǎn)不同塑陵,h函數(shù)的第二個(gè)參數(shù)感憾,如果是數(shù)組的話表示子節(jié)點(diǎn)列表
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 創(chuàng)建包含子節(jié)點(diǎn)的VNode
// h的參數(shù)二為子節(jié)點(diǎn)列表,內(nèi)部就應(yīng)該傳入vNode
let vNode = h('div#container', [
h('h1', '標(biāo)題1'),
h('p', '內(nèi)容1')
])
// 獲取掛載元素
const dom = document.querySelector('#app')
// 渲染vNode
patch(dom, vNode)
函數(shù)參數(shù)為令花!時(shí)表示清空
模塊使用
- 模塊的作用
- 官方提供的模塊
- 模塊的使用步驟
模板的作用
- Snabbdom的核心庫(kù)并不能處理DOM元素的屬性/樣式/事件等等阻桅,可以通過(guò)注冊(cè)Snabbdom默認(rèn)提供的模塊來(lái)實(shí)現(xiàn)
- Snabbdom中的模塊可以用來(lái)拓展Snabbdom的功能
- Snabbdom中的模塊的實(shí)現(xiàn)是可以通過(guò)注冊(cè)全局的鉤子函數(shù)來(lái)實(shí)現(xiàn)的
官方提供的模塊
- attributes
- props
- dataset
- class
- style
- eventlisteners
模塊使用步驟
- 導(dǎo)入需要的模塊
- init()中注冊(cè)模塊
- h函數(shù)的第二個(gè)參數(shù)處使用模塊
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1.導(dǎo)入模塊(注意拼寫(xiě),導(dǎo)入的名稱不要寫(xiě)錯(cuò))
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
console.log(styleModule);
console.log(eventListenersModule)
// 2.注冊(cè)模塊(為patch函數(shù)添加模塊對(duì)應(yīng)的能力)
const patch = init([
styleModule,
eventListenersModule
])
// 3.使用模塊
let vNode = h('div#box', {
style: {
backgroundColor: 'green',
height: '200px',
width: '200px'
}
}, [
h('h1#title', {
style: {
color: '#fff'
},
on: {
click () {
console.log('點(diǎn)擊了h1標(biāo)簽')
}
}
}, '這是標(biāo)題內(nèi)容'),
h('p', '這是內(nèi)容文本')
])
const dom = document.getElementById('app')
patch(dom, vNode)
Snabbdom源碼解析
我們?cè)撛趺纯丛创a呢?
- 宏觀了解
- 帶著目標(biāo)看源碼
- 看源碼的過(guò)程要不求甚解
- 調(diào)試
- 參考文檔資料
Snabbdom的核心
- init()設(shè)置模塊兼都,創(chuàng)建patch函數(shù)
- 使用h函數(shù)創(chuàng)建JS對(duì)象(VNode)描述真實(shí)DOM
- patch()比較新舊兩個(gè)VNode
- 把變化的內(nèi)容更新到真實(shí)的DOM樹(shù)
源碼
- 地址:
- 克隆代碼
git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
h函數(shù)
- 作用:創(chuàng)建VNode對(duì)象
-
Vue的h函數(shù)
函數(shù)重載
- 參數(shù)個(gè)數(shù)或者參數(shù)類型不同的函數(shù)
- JS沒(méi)有重載的概念
- TypeScript中有重載嫂沉,不過(guò)重載的實(shí)現(xiàn)還是通過(guò)代碼調(diào)整參數(shù)
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)是否有變化
init
patch
createElm
在patch函數(shù)中使用到的createElm
patchVnode
updateChildren
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key as string]
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
注:后續(xù)會(huì)專門(mén)挨著挨著解析源碼的作用