vue 虛擬DOM快速入門
虛擬 DOM
什么是虛擬 dom
dom 是文檔對象模型,以節(jié)點(diǎn)樹的形式來表現(xiàn)文檔。
虛擬 dom 不是真正意義上的 dom。而是一個 javascript 對象。
正常的 dom 節(jié)點(diǎn)在 html 中是這樣表示:
<div class='testId'>
<p>你好</p>
<p>歡迎光臨</p>
</div>
而在虛擬 dom 中大概是這樣:
{
tag: 'div',
attributes:{
class: ['testId']
},
children:[
// p 元素
// p 元素
]
}
我們可以將虛擬 dom 拆分成兩部分進(jìn)行理解:虛擬 + dom。
- 虛擬: 表示虛擬 dom 不是真正意義上的 dom,而是一個 javascript 對象;
- dom: 表示虛擬 dom 能以類似節(jié)點(diǎn)樹的形式表示文檔。
虛擬 dom 的作用
現(xiàn)在主流的框架都是聲明式操作 dom 的框架。我們只需要描述狀態(tài)與 dom 之間的映射關(guān)系即可,狀態(tài)到視圖(真實(shí)的 dom)的轉(zhuǎn)換,框架會幫我們做。
最粗暴的做法是將狀態(tài)渲染成視圖,每次更新狀態(tài),都重新更新整個視圖。
這種做法的性能可想而知。比較好的想法是:狀態(tài)改變,只更新與狀態(tài)相關(guān)的 dom 節(jié)點(diǎn)。虛擬 dom 只是實(shí)現(xiàn)這個想法的其中一種方法而已。
具體做法:
- 狀態(tài) -> 真實(shí) dom(最初)
- 狀態(tài) -> 虛擬 dom -> 真實(shí) dom(使用虛擬 dom)
狀態(tài)改變,重新生成一份虛擬 dom,將上一份和這一份虛擬 dom 進(jìn)行對比,找出需要更新的部分,更新真實(shí) dom。
vue 中的虛擬 dom
真實(shí)的 dom 是由 節(jié)點(diǎn)(Node)組成,虛擬 dom 則是由虛擬節(jié)點(diǎn)(vNode)組成。
虛擬 dom 在 vue 中主要做兩件事:
- 提供與真實(shí)節(jié)點(diǎn)(Node)對應(yīng)的虛擬節(jié)點(diǎn)(vNode)
- 將新的虛擬節(jié)點(diǎn)與舊的虛擬節(jié)點(diǎn)進(jìn)行對比,找出需要差異,然后更新視圖
“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼 —— vue 官網(wǎng)
vNode
什么是 vNode
上文提到,vNode(虛擬節(jié)點(diǎn))對應(yīng)的是真實(shí)節(jié)點(diǎn)(Node)。
vNode 可以理解成節(jié)點(diǎn)描述對象。描述了如何創(chuàng)建真實(shí)的 dom 節(jié)點(diǎn)。
vue.js 中有一個 vNode 類??梢允褂盟鼊?chuàng)建不同類型的 vNode 實(shí)例,不同類型的 vNode 對應(yīng)著不同類型的 dom 元素。代碼如下:
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
}
}
從代碼不難看出 vNode 類創(chuàng)建的實(shí)例,本質(zhì)上就是一個普通的 javascript 對象。
vNode 的類型
前面我們已經(jīng)介紹通過 vNode 類可以創(chuàng)建不同類型的 vNode。而不同類型的 vNode 是由有效屬性區(qū)分。例如 isComment = true 表示注釋節(jié)點(diǎn);isCloned = true 表示克隆節(jié)點(diǎn)等等。
vNode 類型有:注釋節(jié)點(diǎn)、文本節(jié)點(diǎn)、克隆節(jié)點(diǎn)、元素節(jié)點(diǎn)、組件節(jié)點(diǎn)。
以下是注釋節(jié)點(diǎn)、文本節(jié)點(diǎn)和克隆節(jié)點(diǎn)的代碼:
/*
注釋節(jié)點(diǎn)
有效屬性:{isComment: true, text: '注釋節(jié)點(diǎn)'}
*/
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
// 注釋
node.isComment = true
return node
}
/*
文本節(jié)點(diǎn)
有效屬性:{text: '文本節(jié)點(diǎn)'}
*/
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// 用于靜態(tài)節(jié)點(diǎn)和插槽節(jié)點(diǎn)
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
// 克隆節(jié)點(diǎn)
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
// 標(biāo)記是克隆節(jié)點(diǎn)
cloned.isCloned = true
return cloned
}
克隆節(jié)點(diǎn)其實(shí)就是將現(xiàn)有節(jié)點(diǎn)的所有屬性賦值到新節(jié)點(diǎn)中,最后用 cloned.isCloned = true 標(biāo)記自身是克隆節(jié)點(diǎn)。
元素節(jié)點(diǎn)通常有以下 4 個屬性:
- tag:節(jié)點(diǎn)名稱。例如 div、p
- data:節(jié)點(diǎn)上的數(shù)據(jù)。例如 class、style
- children:子節(jié)點(diǎn)
- context:在組件內(nèi)呈現(xiàn)
組件節(jié)點(diǎn)與元素節(jié)點(diǎn)類似,包含兩個獨(dú)有的屬性:
- componentOptions:組件節(jié)點(diǎn)的選項(xiàng)參數(shù),例如propsData、listeners、children、tag
- componentInstance:組件的實(shí)例
patch
前面已經(jīng)介紹了虛擬 dom 在 vue 中做的第一件事:提供與真實(shí)節(jié)點(diǎn)(Node)對應(yīng)的虛擬節(jié)點(diǎn)(vNode);接下來介紹第二件事:將新的虛擬節(jié)點(diǎn)與舊的虛擬節(jié)點(diǎn)進(jìn)行對比,找出需要差異,然后更新視圖。
第二件事在 vue 中的實(shí)現(xiàn)叫做 patch,即打補(bǔ)丁、修補(bǔ)的意思。通過對比新舊 vNode,找出差異,然后在現(xiàn)有 dom 的基礎(chǔ)上進(jìn)行修補(bǔ),從而實(shí)現(xiàn)視圖更新。
對比 vNode 找差異是手段,更新視圖才是目的。
而更新視圖無非就是新增節(jié)點(diǎn)、刪除節(jié)點(diǎn)和更新節(jié)點(diǎn)。接下來我們逐一分析什么時候新增節(jié)點(diǎn)、在哪里新增;什么時候刪除節(jié)點(diǎn),刪除哪個;什么時候更新節(jié)點(diǎn),更新哪個;
注:當(dāng) vNode 與 oldVNode 不相同的時候,以 vNode 為準(zhǔn)。
新增節(jié)點(diǎn)
一種情況是:vNode 存在而 oldVNode 不存在時,需要新增節(jié)點(diǎn)。最典型的是初次渲染,因?yàn)?odlVNode 是不存在的。
另一種情況是 vNode 與 oldVNode 完全不是同一個節(jié)點(diǎn)。這時就需要使用 vNode 生成真實(shí)的 dom 節(jié)點(diǎn)并插入到 oldVNode 指向的真實(shí) dom 節(jié)點(diǎn)旁邊。oldVNode 則是一個被廢棄的節(jié)點(diǎn)。例如下面這種情況:
<div>
<p v-if="type === 'A'">
我是節(jié)點(diǎn)A
</p>
<span v-else-if="type === 'B'">
我是與A完全不同的節(jié)點(diǎn)B
</span>
</div>
當(dāng) type 由 A 變?yōu)?B,節(jié)點(diǎn)就會從 p 變成 span,由于 vNode 與 oldVNode 完全不是同一個節(jié)點(diǎn),所以需要新增節(jié)點(diǎn)。
刪除節(jié)點(diǎn)
當(dāng)節(jié)點(diǎn)只在 oldVNode 中存在時,直接將其刪除即可。
更新節(jié)點(diǎn)
前面介紹了新增節(jié)點(diǎn)和刪除節(jié)點(diǎn)的場景,發(fā)現(xiàn)它們有一個共同點(diǎn):vNode 與 oldVNode 完全不相同。
但更常見的場景是 vNode 與 oldVNode 是同一個節(jié)點(diǎn)。然后我們需要對它們(vNode 與 oldVNode)進(jìn)行一個更細(xì)致的對比,再對 oldVNode 對應(yīng)的真實(shí)節(jié)點(diǎn)進(jìn)行更新。
對于文本節(jié)點(diǎn),邏輯自然簡單。首先對比新舊 vNode,發(fā)現(xiàn)是同一個節(jié)點(diǎn),然后將 oldVNode 對應(yīng)的 dom 節(jié)點(diǎn)的文本改成 vNode 中的文本即可。但對于復(fù)雜的 vNode,比如界面中的一顆樹組件,這個過程就會變得復(fù)雜。
新增節(jié)點(diǎn) - 源碼分析
思考一下:前面說到 vNode 的類型有:注釋節(jié)點(diǎn)、文本節(jié)點(diǎn)、克隆節(jié)點(diǎn)、元素節(jié)點(diǎn)、組件節(jié)點(diǎn)。請問這幾種類型都會被創(chuàng)建并插入到 dom 中嗎?
答:只有注釋節(jié)點(diǎn)、文本節(jié)點(diǎn)、元素節(jié)點(diǎn)。因?yàn)?html 只認(rèn)識這幾種。
由于只有上面三種節(jié)點(diǎn)類型,根據(jù)類型做響應(yīng)的創(chuàng)建,然后插入對應(yīng)的位置即可。
以元素節(jié)點(diǎn)為例,如果 vNode 有 tag 屬性,則說明是元素節(jié)點(diǎn)。則調(diào)用 createElement 方法創(chuàng)建對應(yīng)的節(jié)點(diǎn),接下來就通過 appendChild 方法插入到指定父節(jié)點(diǎn)中。如果父元素已經(jīng)在視圖中,那么把元素插入到它下面將會自動渲染出來;如果 vNode 的 isComment 屬性是 true,則表示注釋節(jié)點(diǎn);都不是則是文本節(jié)點(diǎn);
通常元素里面會有子節(jié)點(diǎn),所以這里涉及一個遞歸的過程,也就是將 vNode 中的 children 依次遍歷,創(chuàng)建節(jié)點(diǎn),然后插入到父節(jié)點(diǎn)(父節(jié)點(diǎn)也就是剛剛創(chuàng)建出的 dom 節(jié)點(diǎn))中,一層一層的遞歸進(jìn)行。
請看源碼:
// 創(chuàng)建元素
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
// 有 tag 屬性,表示是元素節(jié)點(diǎn)
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
// 創(chuàng)建元素。nodeOps 涉及到跨平臺
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
// 遞歸創(chuàng)建子節(jié)點(diǎn),并將子節(jié)點(diǎn)插入到父節(jié)點(diǎn)上
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 將 vnode 對應(yīng)的元素插入到父元素中
insert(parentElm, vnode.elm, refElm);
}
// isComment 屬性表示注釋節(jié)點(diǎn)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
// 插入父節(jié)點(diǎn)
insert(parentElm, vnode.elm, refElm);
// 否則就是子節(jié)點(diǎn)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
// 插入父節(jié)點(diǎn)
insert(parentElm, vnode.elm, refElm);
}
}
// 遞歸創(chuàng)建子節(jié)點(diǎn),并將子節(jié)點(diǎn)插入到父節(jié)點(diǎn)上。vnode 表示父節(jié)點(diǎn)
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children);
}
// 依次創(chuàng)建子節(jié)點(diǎn),并將子節(jié)點(diǎn)插入到父節(jié)點(diǎn)中
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
刪除節(jié)點(diǎn) - 源碼分析
刪除節(jié)點(diǎn)非常簡單。直接看源碼:
// 刪除一組指定節(jié)點(diǎn)
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else { // Text node
// 刪除個節(jié)點(diǎn)
removeNode(ch.elm);
}
}
}
}
// 刪除單個節(jié)點(diǎn)
function removeNode (el) {
var parent = nodeOps.parentNode(el);
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
// nodeOps里封裝了跨平臺的方法
nodeOps.removeChild(parent, el);
}
}
以上就是vue 虛擬DOM快速入門的詳細(xì)內(nèi)容,更多關(guān)于vue 虛擬DOM的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue中使用console.log打印的實(shí)現(xiàn)
這篇文章主要介紹了vue中使用console.log打印的實(shí)現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
利用vue開發(fā)一個所謂的數(shù)獨(dú)方法實(shí)例
數(shù)獨(dú)是源自18世紀(jì)瑞士的一種數(shù)學(xué)游戲,是一種運(yùn)用紙、筆進(jìn)行演算的邏輯游戲。下面這篇文章主要給大家介紹了關(guān)于利用vue開發(fā)一個所謂的數(shù)獨(dú)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12
關(guān)于vue.js彈窗組件的知識點(diǎn)總結(jié)
最近在開發(fā)過程對對于組件化的開發(fā)有一些感想,于是開始記錄下這些。彈窗組件一直是 web 開發(fā)中必備的,使用頻率相當(dāng)高,最常見的莫過于 alert,confirm和prompt這些,不同的組件庫對于彈窗的處理也是不一樣的,下面來一起看看吧。2016-09-09
解決axios發(fā)送post請求返回400狀態(tài)碼的問題
今天小編就為大家分享一篇解決axios發(fā)送post請求返回400狀態(tài)碼的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08
基于 vue-skeleton-webpack-plugin 的骨架屏實(shí)戰(zhàn)
這篇文章主要介紹了基于 vue-skeleton-webpack-plugin 的骨架屏實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Vue+Openlayer實(shí)現(xiàn)圖形的拖動和旋轉(zhuǎn)變形效果
Openlayer具有自己的擴(kuò)展插件ol-ext,可以用來實(shí)現(xiàn)圖形的拖拽、旋轉(zhuǎn)、縮放、拉伸、移動等操作,本文將主要介紹通過Openlayer實(shí)現(xiàn)圖形的拖動和旋轉(zhuǎn),需要的同學(xué)可以學(xué)習(xí)一下2021-11-11

