淺析Vue中Virtual?DOM和Diff原理及實(shí)現(xiàn)
0. 寫在開頭
本文將秉承Talk is cheap, show me the code原則,做到文字最精簡,一切交由代碼說明!
1. vdom
vdom即虛擬DOM,將DOM映射為JS對象,結(jié)合diff算法更新DOM
以下為DOM
<div id="app"> <div class="home">home</div> </div>
映射成VDOM
{
tag: 'div',
attrs: {
id: 'app'
},
children: [
{
tag: 'div',
attrs: {
class: 'home'
},
children: [
{
tag: undefined,
attrs: undefined,
text: 'home',
children: undefined
}
]
}
]
}通過這個(gè)vdom實(shí)現(xiàn)簡單的render函數(shù),可以通過js操作修改dom
<template>
<div id="app">
<div v-for="item in arr">{{ item.name }} : {{ item.id }}</div>
</div>
<button id="btn">reRender</button>
</template>
let app = document.getElementById('app')
let data = {
arr: [
{ name: 'a', id: 1 },
{ name: 'b', id: 2 },
{ name: 'c', id: 3 },
]
}
function render(data) {
app.innerHtml = ''
let children = []
data.forEach(item => {
let el = document.createElement("div")
el.innerHtml = `${ item.name } : ${item.id}`
app.appendChild(el)
})
}
// test
render(data.arr) // 首次渲染
let btn = document.getElementById('btn')
btn.onClick = () => {
data.arr[2].id++ // 修改關(guān)聯(lián)數(shù)據(jù)
render(data.arr) // 重新渲染:暴力刷新DOM,沒有diff,實(shí)際上只用更新最后一個(gè)div就行
}使用snabbdom實(shí)現(xiàn)VDOM
snabbldom是簡易實(shí)現(xiàn)vdom功能的庫,有兩個(gè)核心api:h函數(shù)和patch函數(shù)
h(tag, attrs, children) // 創(chuàng)建vnode patch(vnode, newVnode) // 對vnode進(jìn)行diff后掛載到真實(shí)dom上
結(jié)合h和patch實(shí)現(xiàn)render渲染函數(shù)
let app = document.getElementById('app')
let vnode;
function render(data) {
let newVnode = h('div', { class: 'wrap' }, data.forEach(item => {
return h('div', {}, `${item.name} : ${item.id}`)
})
)
patch(vnode, newVnode)
vnode = newVnode
}
render(data.arr) // 首次渲染
let btn = document.getElementById('btn')
btn.onClick = () => {
data.arr[2].id++ // 修改關(guān)聯(lián)數(shù)據(jù)
render(data.arr) // 重新渲染:在patch函數(shù)里經(jīng)過vdom的diff后再掛載到真實(shí)dom,這里只更新最后一個(gè)div
}2. Diff
為了盡量減少DOM操作,需要通過diff對比新舊vnode,針對更改的地方進(jìn)行更新DOM,而非替換整個(gè)DOM
大體思路為:
- 對新舊兩個(gè)節(jié)點(diǎn)調(diào)用
patch函數(shù) - 進(jìn)來先判斷兩個(gè)節(jié)點(diǎn)是否為同一類型,具體是對比
key、tag、data等屬性 - 若不為同一類型,那么基于新節(jié)點(diǎn)創(chuàng)建dom之后作替換
- 若為同一類型,那么調(diào)用
patchVnode函數(shù) - 進(jìn)來先判斷兩個(gè)節(jié)點(diǎn)是文本節(jié)點(diǎn)的話,那么就作文本內(nèi)容替換
- 否則判斷是否都有子節(jié)點(diǎn),都有的話調(diào)用
updateChildren函數(shù),通過首尾四個(gè)指針對子節(jié)點(diǎn)數(shù)組進(jìn)行diff更新;若舊節(jié)點(diǎn)有子節(jié)點(diǎn),新節(jié)點(diǎn)沒有,這時(shí)就刪除子節(jié)點(diǎn);若舊節(jié)點(diǎn)無子節(jié)點(diǎn),新節(jié)點(diǎn)有,這時(shí)基于新節(jié)點(diǎn)創(chuàng)建dom作替換即可
通過createElment函數(shù),將VDOM轉(zhuǎn)為真實(shí)DOM
function createElement(vnode) {
if(vnode.text) return document.createTextNode(vnode) // 文本節(jié)點(diǎn)
let { tag, attrs, children } = vnode
let el = document.createElement(tag) // tag
for(let key of attrs){ // attrs
el.setAttribute(key, attrs[key])
}
children.forEach(childVnode => { // children
el.appendChild(createElement(childVnode))
})
vnode.el = el
return el
}通過patch函數(shù),執(zhí)行diff更新操作
判斷vnode和newVnode是否為同一類型節(jié)點(diǎn),是則繼續(xù)遞歸對比子節(jié)點(diǎn),否則直接替換
function patch(vnode, newVnode) {
if (isSameNode(vnode, newVnode)) patchVnode(vnode, newVnode)
else replaceVnode(vnode, newVnode)
}
function replaceVnode(vnode, newVnode) {
let el = vnode.el // 舊節(jié)點(diǎn)
let parentEl = api.getParentNode(el) // 獲取父節(jié)點(diǎn)
api.insertBefore(parentEl, createElement(newVnode), api.getNextSibling(el)) // 插入新節(jié)點(diǎn)
api.removeChild(parentEl, el) // 刪除舊節(jié)點(diǎn)
}
function isSameNode(vnode, newVnode) {
return (
vnode.key == newVnode.key && // key是否相同
vnode.tag == newVnode.tag && // tag是否相同
isDef(vnode.data) == isDef(newVnode.data) // 是否都定義了data
// &&... 其他條件
)
}
function patchVnode(vnode, newVnode) {
let el = newVnode.el = vnode.el // 獲取當(dāng)前舊節(jié)點(diǎn)對應(yīng)的dom,并賦值給新節(jié)點(diǎn)的el
// 1.都為文本節(jié)點(diǎn),且文本不一樣
if (vnode.text && newVnode.text && vnode.text != newVnode.text)
return api.setElText(el, newVnode.text) // 替換文本
let ch = vnode.children
let newCh = newVnode.children
if (ch && newCh) return updateChildren(el, ch, newCh) // 2.都有子節(jié)點(diǎn),遞歸對比
if (ch) return api.removeChild(el) // 3.vnode有子節(jié)點(diǎn),newVnode無,刪除子節(jié)點(diǎn)
return replaceVnode(vnode, newVnode) // 4. newNode有子節(jié)點(diǎn),vnode無,替換即可
}updateChildren實(shí)現(xiàn)比較復(fù)雜,使用首尾四指針進(jìn)行vnode和newVnode的對比
function updateChildren(el, ch, newCh) {
// 子節(jié)點(diǎn)下標(biāo)
let l = 0
let r = ch.length - 1
let newL = 0
let newR = newCh.length - 1
// 子節(jié)點(diǎn)
let lNode = ch[l]
let rNode = ch[r]
let newLNode = newCh[newL]
let newRNode = newCh[newR]
while (l <= r && newL <= newR) {
if (!lNode || !rNode || !newLNode || !newRNode) { // 邊界處理
if (!lNode) lNode = ch[++l]
if (!rNode) rNode = ch[--r]
if (!newLNode) newLNode = newCh[++newL]
if (!newRNode) newRNode = newCh[--newR]
continue
}
// 新舊子節(jié)點(diǎn)首尾指針對比 l*newL、r*newR、l*newR、r*newL
if (isSameNode(lNode, newLNode)) {
patchVnode(lNode, newLNode)
lNode = ch[++l]
newLNode = newCh[++newL]
continue
}
if (isSameNode(rNode, newRNode)) {
patchVnode(rNode, newRNode)
rNode = ch[--r]
newRNode = newCh[--newR]
continue
}
if (isSameNode(lNode, newRNode)) {
patchVnode(lNode, newRNode)
api.insertBefore(el, lNode.el, api.nextSibling(rNode.el))
lNode = ch[++l]
newRNode = newCh[--newR]
continue
}
if (isSameNode(rNode, newLNode)) {
patchVnode(rNode, newLNode)
api.insertBefore(el, rNode.el, lNode.el)
rNode = ch[--r]
newLNode = newCh[++newL]
continue
}
// 在vnode未知序列區(qū)間[l,r]生成key-idx的map表,用newLNode的key在未知序列中找到可復(fù)用的位置
if (!keyIdxMap) keyIdxMap = getKeyIdxMap(ch, l, r) // map
keyIdx = keyIdxMap.get(newLNode.key)
if (!keyIdx) {
api.insertBefore(el, createElement(newLNode), lNode.el)
}
else {
let nodeToMove = ch[keyIdx]
patchVnode(nodeToMove, newLNode)
api.insertBefore(el, nodeToMove.el, lNode.el)
}
newLNode = newCh[++newL]
}
}
function getKeyIdxMap(ch, l, r) {
let map = new Map()
while (l <= r) map.set(ch[l].key, l++)
return map
}到此這篇關(guān)于淺析Vue中Virtual DOM和Diff原理及實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vue Virtual DOM Diff內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue實(shí)現(xiàn)Google第三方登錄的示例代碼
本文記錄作者在vue項(xiàng)目中使用到Google第三方登錄,查詢到的資料文檔也不詳細(xì),故此把自己所遇到的坑及問題詳細(xì)的記錄下來。2021-07-07
element-ui表格數(shù)據(jù)轉(zhuǎn)換的示例代碼
這篇文章主要介紹了element-ui表格數(shù)據(jù)轉(zhuǎn)換的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
使用Karma做vue組件單元測試的實(shí)現(xiàn)
這篇文章主要介紹了使用Karma做vue組件單元測試的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
vue使用screenfull插件實(shí)現(xiàn)全屏功能
這篇文章主要為大家詳細(xì)介紹了vue使用screenfull插件實(shí)現(xiàn)全屏功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
Vue登錄頁面的動(dòng)態(tài)粒子背景插件實(shí)現(xiàn)
本文主要介紹了Vue登錄頁面的動(dòng)態(tài)粒子背景插件實(shí)現(xiàn),將登錄組件背景設(shè)置為 "粒子背景",具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
VUE使用docxtemplater導(dǎo)出word文檔實(shí)例(帶圖片)
docxtemplate支持的功能很多,語法包含變量替換、條件判斷、循環(huán)、列表循環(huán)、表格循環(huán)等,下面這篇文章主要給大家介紹了關(guān)于VUE使用docxtemplater導(dǎo)出word功能(帶圖片)的相關(guān)資料,需要的朋友可以參考下2023-06-06
快速解決 keep-alive 緩存組件中定時(shí)器干擾問題
文章介紹了在使用keep-alive緩存組件時(shí),如何在組件被緩存后清理定時(shí)器以避免干擾其他組件的邏輯,通過在deactivated鉤子中清理定時(shí)器,可以確保組件被緩存時(shí)不會繼續(xù)運(yùn)行定時(shí)器,感興趣的朋友一起看看吧2025-02-02
使用vue-virtual-scroller遇到的問題及解決
這篇文章主要介紹了使用vue-virtual-scroller遇到的問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
Vue 實(shí)現(xiàn)輸入框新增搜索歷史記錄功能
這篇文章主要介紹了Vue 輸入框新增搜索歷史記錄功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10

