Virtual DOM

這次我們的學(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操作方式:

https://codesandbox.io/s/jquery-demo-yr65q

虛擬DOM操作方式:

https://obk5t.csb.app/

通過(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 文檔

https://github.com/snabbdom/snabbdom

當(dāng)前版本:v2.1.0
官方文檔中文翻譯:

https://github.com/coconilu/Blog/issues/152

根據(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ù)

源碼

  • 地址:

https://github.com/snabbdom/snabbdom

  • 克隆代碼

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ù)
參數(shù)個(gè)數(shù)

參數(shù)類型

h函數(shù)的重載

內(nèi)部本身只做判斷

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

鉤子函數(shù)與init構(gòu)造函數(shù)

patch

patch函數(shù)

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)挨著挨著解析源碼的作用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末北戏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子漫蛔,更是在濱河造成了極大的恐慌嗜愈,老刑警劉巖旧蛾,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蠕嫁,居然都是意外死亡锨天,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)剃毒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)病袄,“玉大人,你說(shuō)我怎么就攤上這事赘阀∫娌” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵基公,是天一觀的道長(zhǎng)幅慌。 經(jīng)常有香客問(wèn)我轰豆,道長(zhǎng)酸休,這世上最難降的妖魔是什么菩咨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮糙置,結(jié)果婚禮上是目,老公的妹妹穿的比我還像新娘懊纳。我一直安慰自己嗤疯,他們只是感情好戏罢,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布桐磁。 她就那樣靜靜地躺著,像睡著了一般我擂。 火紅的嫁衣襯著肌膚如雪郎任。 梳的紋絲不亂的頭發(fā)上霉猛,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死轩猩,一個(gè)胖子當(dāng)著我的面吹牛晤锹,可吹牛的內(nèi)容都是我干的薇宠。 我是一名探鬼主播回梧,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼祖搓!你這毒婦竟也來(lái)了狱意?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拯欧,失蹤者是張志新(化名)和其女友劉穎详囤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體镐作,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藏姐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了该贾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羔杨。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖杨蛋,靈堂內(nèi)的尸體忽然破棺而出兜材,到底是詐尸還是另有隱情,我是刑警寧澤逞力,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布曙寡,位于F島的核電站,受9級(jí)特大地震影響掏击,放射性物質(zhì)發(fā)生泄漏卵皂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一砚亭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧殴玛,春花似錦捅膘、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至凡壤,卻和暖如春署尤,著一層夾襖步出監(jiān)牢的瞬間耙替,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工曹体, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俗扇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓箕别,卻偏偏與公主長(zhǎng)得像铜幽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子串稀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容