淺析Vue中Virtual?DOM和Diff原理及實(shí)現(xiàn)
0. 寫(xiě)在開(kāi)頭
本文將秉承Talk is cheap, show me the code原則,做到文字最精簡(jiǎn),一切交由代碼說(shuō)明!
1. vdom
vdom即虛擬DOM,將DOM映射為JS對(duì)象,結(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
}
]
}
]
}通過(guò)這個(gè)vdom實(shí)現(xiàn)簡(jiǎn)單的render函數(shù),可以通過(guò)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,沒(méi)有diff,實(shí)際上只用更新最后一個(gè)div就行
}使用snabbdom實(shí)現(xiàn)VDOM
snabbldom是簡(jiǎn)易實(shí)現(xiàn)vdom功能的庫(kù),有兩個(gè)核心api:h函數(shù)和patch函數(shù)
h(tag, attrs, children) // 創(chuàng)建vnode patch(vnode, newVnode) // 對(duì)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)過(guò)vdom的diff后再掛載到真實(shí)dom,這里只更新最后一個(gè)div
}2. Diff
為了盡量減少DOM操作,需要通過(guò)diff對(duì)比新舊vnode,針對(duì)更改的地方進(jìn)行更新DOM,而非替換整個(gè)DOM
大體思路為:
- 對(duì)新舊兩個(gè)節(jié)點(diǎn)調(diào)用
patch函數(shù) - 進(jìn)來(lái)先判斷兩個(gè)節(jié)點(diǎn)是否為同一類型,具體是對(duì)比
key、tag、data等屬性 - 若不為同一類型,那么基于新節(jié)點(diǎn)創(chuàng)建dom之后作替換
- 若為同一類型,那么調(diào)用
patchVnode函數(shù) - 進(jìn)來(lái)先判斷兩個(gè)節(jié)點(diǎn)是文本節(jié)點(diǎn)的話,那么就作文本內(nèi)容替換
- 否則判斷是否都有子節(jié)點(diǎn),都有的話調(diào)用
updateChildren函數(shù),通過(guò)首尾四個(gè)指針對(duì)子節(jié)點(diǎn)數(shù)組進(jìn)行diff更新;若舊節(jié)點(diǎn)有子節(jié)點(diǎn),新節(jié)點(diǎn)沒(méi)有,這時(shí)就刪除子節(jié)點(diǎn);若舊節(jié)點(diǎn)無(wú)子節(jié)點(diǎn),新節(jié)點(diǎn)有,這時(shí)基于新節(jié)點(diǎn)創(chuàng)建dom作替換即可
通過(guò)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
}通過(guò)patch函數(shù),執(zhí)行diff更新操作
判斷vnode和newVnode是否為同一類型節(jié)點(diǎn),是則繼續(xù)遞歸對(duì)比子節(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)對(duì)應(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),遞歸對(duì)比
if (ch) return api.removeChild(el) // 3.vnode有子節(jié)點(diǎn),newVnode無(wú),刪除子節(jié)點(diǎn)
return replaceVnode(vnode, newVnode) // 4. newNode有子節(jié)點(diǎn),vnode無(wú),替換即可
}updateChildren實(shí)現(xiàn)比較復(fù)雜,使用首尾四指針進(jìn)行vnode和newVnode的對(duì)比
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)首尾指針對(duì)比 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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue+axios+java實(shí)現(xiàn)文件上傳功能
這篇文章主要為大家詳細(xì)介紹了vue+axios+java實(shí)現(xiàn)文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
undefined是否會(huì)變?yōu)閚ull原理解析
這篇文章主要為大家介紹了undefined是否會(huì)變?yōu)閚ull原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
vue+element實(shí)現(xiàn)輸入密碼鎖屏
這篇文章主要為大家詳細(xì)介紹了vue+element實(shí)現(xiàn)輸入密碼鎖屏,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
vue使用keep-alive如何實(shí)現(xiàn)多頁(yè)簽并支持強(qiáng)制刷新
這篇文章主要介紹了vue使用keep-alive如何實(shí)現(xiàn)多頁(yè)簽并支持強(qiáng)制刷新,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Vux+Axios攔截器增加loading的問(wèn)題及實(shí)現(xiàn)方法
這篇文章主要介紹了Vux+Axios攔截器增加loading的問(wèn)題及實(shí)現(xiàn)方法,文中通過(guò)實(shí)例代碼介紹了vue中使用axios的相關(guān)知識(shí),需要的朋友可以參考下2018-11-11
vue中v-for循環(huán)選中點(diǎn)擊的元素并對(duì)該元素添加樣式操作
這篇文章主要介紹了vue中v-for循環(huán)選中點(diǎn)擊的元素并對(duì)該元素添加樣式操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-07-07

