您好,登錄后才能下訂單哦!
這篇文章主要介紹“Virtual DOM作用是什么”,在日常操作中,相信很多人在Virtual DOM作用是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Virtual DOM作用是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
Virtual DOM(虛擬DOM),在形態上表現為一個能夠描述DOM結構及其屬性信息的普通的JS對象,因為不是真實的DOM對象,所以叫虛擬DOM。
<div></div>
{ sel: 'div', data: {}, chidren:undefined, elm:undefined, key:undefined, }
Virtual DOM 本質上JS和DOM之間的一個映射緩存。可以類比 CPU 和硬盤,既然硬盤這么慢,我們就在它們之間加個緩存:既然 DOM 這么慢,我們就在它們 JS 和 DOM 之間加個緩存。CPU(JS)只操作內存(Virtual DOM),最后的時候再把變更寫入硬盤(DOM)。
手動操作DOM比較麻煩,還需要考慮瀏覽器兼容性問題,雖然有jquery等庫簡化DOM操作,但是隨著項目的復雜DOM操作復雜提升。
為了簡化DOM的復雜操作于是出現了MVVM框架,MVVM框架解決了視圖和狀態的同步問題。
為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態變化的問題,于是Virtual DOM出現了。
Virtual DOM的好處是當狀態改變時不需要立即更新DOM,只需要創建一個虛擬樹來描述DOM,Virtual DOM內部將弄清除如何有效(diff)的更新DOM。
虛擬DOM可以維護程序的狀態,跟蹤上一次的狀態,通過比較前后兩次狀態的差異更新真實DOM。
真實DOM 因為瀏覽器廠商需要實現眾多的規范(各種 HTML5 屬性、DOM事件),即使創建一個空的 div 也要付出昂貴的代價。如以下代碼,打印空的div屬性一共298個。而這僅僅是第一層。真正的 DOM 元素非常龐大。直接操作DOM可能會導致頻繁的回流和重繪。
const div = document.createElement('div'); const arr = []; for(key in div){arr.push(key)}; console.log(arr.length); // 298
對復雜的文檔DOM結構(復雜視圖情況下提升渲染性能),提供一種方便的工具,進行最小化地DOM操作。既然我們可以用JS對象表示DOM結構,那么當數據狀態發生變化而需要改變DOM結構時,我們先通過JS對象表示的虛擬DOM計算出實際DOM需要做的最小變動(Virtual DOM會使用diff算法計算出如果有效的更新dom,只更新狀態改變的DOM),然后再操作實際DOM,從而避免了粗放式的DOM操作帶來的性能問題,減少對真實DOM的操作。
我們不再需要手動去操作 DOM,只需要寫好 View-Model 的代碼邏輯,MVVM框架會根據虛擬 DOM 和 數據雙向綁定,幫我們以可預期的方式更新視圖,極大提高我們的開發效率。
虛擬DOM是對真實的渲染內容的一層抽象,是真實DOM的描述,因此,它可以實現“一次編碼,多端運行”,可以實現SSR(Nuxt.js/Next.js)、原生應用(Weex/React Native)、小程序(mpvue/uni-app)等。
上面我們也說到了在復雜視圖情況下提升渲染性能。雖然虛擬 DOM + 合理的優化,足以應對絕大部分應用的性能需求,但在一些性能要求極高的應用中虛擬DOM 無法進行針對性的極致優化。首次渲染大量DOM時,由于多了一層虛擬DOM的計算,會比innerHTML插入慢。
下方是尤大自己的見解。https://www.zhihu.com/question/31809713/answer/53544875
一個JavaScript DOM模型,支持元素創建,差異計算和補丁操作,以實現高效的重新渲染。
源代碼庫地址:https://github.com/Matt-Esch/virtual-dom.git
已經有五六年沒有維護了
一個虛擬DOM庫,重點放在簡單性,模塊化,強大的功能和性能上。
源代碼庫地址:https://github.com/snabbdom/snabbdom.git
最近一直在維護
Vue2.x內部使用的Virtual DOM就是改造的Snabbdom;
核心代碼大約200行;
通過模塊可擴展;
源碼使用TypeScript開發;
最快的Virtual DOM之一;
最近在維護
使用 h()函數創建 JavaScript 對象(Vnode)描述真實 DOM
init()設置模塊,創建 patch()
patch()比較新舊兩個 Vnode
把變化的內容更新到真實 DOM 樹上
npm init -y
or
yarn init -y
安裝snabbdom
npm install snabbdom
or
yarn add snabbdom
安裝parcel-bundler
npm install parcel-bundler
or
yarn add parcel-bundler
在根目錄下創建一個名為src的文件目錄,然后在里面創建一個main.js文件。最后,在根目錄下創建一個index.html文件。
package.json文件可以編輯如下,更利于操作。
"scripts": { "serve": "parcel index.html --open", "build": "parcel build index.html" },
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>snabbdomApp</title> </head> <body> <div id="app"></div> <script src="src/main.js"></script> </body> </html>
main.js
主要介紹snabbdom中兩個核心init()、h()。
init() 是一個高階函數,返回patch()(對比兩個VNode的差異更新到真實DOM);
h() 返回虛擬節點VNode;
示例1
import { h, init } from 'snabbdom'; // init函數參數:數組(模塊) // 返回值:patch函數:作用是對比兩個Vnode的差異更新到真實dom const patch = init([]); // h函數 // 第一個參數:標簽+選擇器; // 第二個參數:如果是字符串則是標簽的內容 let vnode = h('div#container', 'Hello World'); const app = document.querySelector('#app'); // patch函數 // 第一個參數:可以是DOM元素(內部會把DOM元素轉化為Vnode),也可以是Vnode; // 第二個參數:Vnode // 返回值:Vnode let oldVnode = patch(app, vnode); // 新舊Vnode對比 vnode = h('div', 'Hello Snabbdom'); patch(oldVnode, vnode);
示例2
import { h, init } from 'snabbdom'; const patch = init([]); // 可放置子元素 let vnode = h('div#container', [h('h2', '1'), h('h3', '2')]); const app = document.querySelector('#app'); const oldVnode = patch(app, vnode); vnode = h('div', 'Hello Snabbdom'); patch(oldVnode, vnode); setInterval(() => { // 清除頁面元素 patch(oldVnode, h('!')); }, 3000);
示例3
常用模塊
The attributes module
設置DOM元素的特性,使用setAttribute添加和更新特性。
The props module
允許設置DOM元素的屬性。
The class module
提供了一種動態切換元素上的類的簡單方法。
The style module
允許在元素上設置CSS屬性。請注意,如果樣式屬性作為樣式對象的屬性被移除,樣式模塊并不會移除它們。為了移除一個樣式,應該將其設置為空字符串。
The dataset module
允許在DOM元素上設置自定義數據屬性(data-*)。
The eventlisteners module
提供了附加事件監聽器的強大功能。
import { h, init, classModule, propsModule, styleModule, eventListenersModule, } from 'snabbdom'; const patch = init([ styleModule, classModule, propsModule, eventListenersModule, ]); let vnode = h( 'div#container', { style: { color: '#000', }, on: { click: eventHandler, }, }, [ h('p', 'p1', h('a', { class: { active: true, selected: true } }, 'Toggle')), h('p', 'p2'), h('a', { props: { href: '/' } }, 'Go to'), ] ); function eventHandler() { console.log('1'); } const app = document.querySelector('#app'); patch(app, vnode);
源碼地址:https://github.com/snabbdom/snabbdom.git以下分析snabbdom版本3.0.1。
核心文件夾是**src目錄。**里面包含了如下文件夾及其目錄:
helpers:里面只有一個文件attachto.ts,這個文件主要作用是定義了幾個類型在vnode.ts文件中使用。
modules:里面存放著snabbdom模塊,分別是attributes.ts、class.ts、dataset.ts、eventlisteners.ts、props.ts、style.ts這6個模塊。另外一個module.ts這個文件為它們提供了鉤子函數。
h.ts:創建Vnode。
hook.ts:提供鉤子函數。
htmldomapi:提供了DOM API。
index.ts:snabbdom 入口文件。
init.ts:導出了patch函數。
is.ts:導出了兩個方法。一個方法是判斷是否是數組,另一個判斷是否是字符串或數字。
jsx.ts:與jsx相關。
thunk.ts:與優化key相關。
tovnode.ts:真實DOM 轉化為 虛擬DOM。
vnode.ts:定義了Vnode的結構。
h.ts
h 函數最早見于 hyperscript,使用 JavaScript 創建超文本,Snabbdom 中的 h 函數不是用來創建超文本,而是創建 Vnode。 在使用 Vue2.x 的時候見過 h 函數,它的參數就是h函數,但是Vue加強了h函數,使其支持組件機制。
new Vue({ router, store, render:h => h(App) }).$mount('#app)
以上是h.ts文件中的內容,可以看到它導出了多個h方法,這種方式叫做函數重載。在JS中暫時沒有,目前TS支持這種機制(但也只是通過調整代碼參數層面上,因為最終TS還是要轉換為JS)。方法名相同,參數個數或類型不同的方法叫做函數重載。所以通過參數個數或類型不同來區分它們。
// 這里通過參數不同來區分不同的函數 function add(a, b) { console.log(a + b); } function add(a, b, c) { console.log(a + b + c); } add(1, 2); add(1, 2, 3);
從上面代碼層面上我們知道了通過函數重載這種方法可以在通過參數個數或類型不同輕松地實現了相應情況調用相應參數的方法。
那么,我們來具體看下源碼是怎么實現函數重載的。
通過源碼我們看到,通過傳入不同的類型的參數調用對應的代碼,最后將將參數傳入到vnode方法中,創建一個Vnode,并返回這個方法。
那么接下來,我們看下vnode方法的實現。
vnode.ts
我們打開vnode.ts這個文件,這個文件主要是導出了一個vnode方法,并且定義了幾個接口。我們看到以下代碼中vnode中的參數含義就知道在h.ts文件中函數參數的意思,是相對應的。
init.ts
在介紹init.ts文件之前的,我們需要知道這樣的一個概念:
init()是一個高階函數,返回patch()
patch(oldVnode,newVnode)
把新節點中的變化的內容渲染到真實DOM,最后返回新節點作為下一次處理的舊節點
這個概念我們在上面已經闡述了。**init()**就是通過這個文件導出的。
在看init.ts源碼之前,我們還需要了解Vnode是渲染到真實DOM的整體流程。這樣,看源碼才不會有誤解。
整體流程:
對比新舊Vnode是否相同節點(節點數據中的key、sel、is相同)
如果不是相同節點,刪除之前的內容,重新渲染
如果是相同節點,再判斷新的Vnode是否有text,如果有并且和oldVnode 的text 不同,直接更新文本內容
如果新的Vnode 有children,判斷子節點是否有變化,判斷子節點的過程就是diff 算法
diff 算法過程只進行同層級節點比較
Diff算法的作用是用來計算出 Virtual DOM 中被改變的部分,然后針對該部分進行原生DOM操作,而不用重新渲染整個頁面。
同級對比
對比的時候,只針對同級的對比,減少算法復雜度。
就近復用
為了盡可能不發生 DOM 的移動,會就近復用相同的 DOM 節點,復用的依據是判斷是否是同類型的 dom 元素。 看到這里你可能就會想到Vue中列表渲染為什么推薦加上key,我們需要使用key來給每個節點做一個唯一標識,Diff算法就可以正確的識別此節點,找到正確的位置區插入新的節點。key的作用主要是為了高效的更新虛擬DOM。
我們先看下init.ts中的大體源碼。
我們先簡單地來看下sameVnode方法。判斷是否是相同的虛擬節點。
// 是否是相同節點 function sameVnode(vnode1: VNode, vnode2: VNode): boolean { const isSameKey = vnode1.key === vnode2.key; const isSameIs = vnode1.data?.is === vnode2.data?.is; const isSameSel = vnode1.sel === vnode2.sel; return isSameSel && isSameKey && isSameIs; }
是否是Vnode。
// 是否是vnode function isVnode(vnode: any): vnode is VNode { return vnode.sel !== undefined; }
注冊一系列的鉤子,在不同的階段觸發。
// 定義一些鉤子函數 const hooks: Array<keyof Module> = [ "create", "update", "remove", "destroy", "pre", "post", ];
下面呢,主要看下導出的init方法。也是init.ts中最主要的部分,從68行到472行。
// 導出init函數 export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) { let i: number; let j: number; const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [], }; // 初始化轉化成虛擬節點的api const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; // 把傳入的所有模塊的鉤子函數,統一存儲到cbs對象中 // 最終構建的cbs對象的形式cbs = {create:[fn1,fn2],update:[],....} for (i = 0; i < hooks.length; ++i) { // cbs.create= [], cbs.update = []... cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { // modules 傳入的模塊數組 // 獲取模塊中的hook函數 // hook = modules[0][create]... const hook = modules[j][hooks[i]]; if (hook !== undefined) { // 把獲取到的hook函數放入到cbs 對應的鉤子函數數組中 (cbs[hooks[i]] as any[]).push(hook); } } } function emptyNodeAt(elm: Element) { const id = elm.id ? "#" + elm.id : ""; const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; return vnode( api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm ); } function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm) as Node; api.removeChild(parent, childElm); } }; } /* 1.觸發鉤子函數init 2.把vnode轉換為DOM對象,存儲到vnode.elm中 - sel是!--》創建注釋節點 - sel不為空 --》創建對應的DOM對象;觸發模塊的鉤子函數create;創建所有子節點對應的DOM對象;觸發鉤子函數create;如果是vnode有inset鉤子函數,追加到隊列 - sel為空 --》創建文本節點 3.返回vnode.elm */ function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any; let data = vnode.data; if (data !== undefined) { // 執行init鉤子函數 const init = data.hook?.init; if (isDef(init)) { init(vnode); data = vnode.data; } } // 把vnode轉換成真實dom對象(沒有渲染到頁面) const children = vnode.children; const sel = vnode.sel; if (sel === "!") { // 如果選擇器是!,創建注釋節點 if (isUndef(vnode.text)) { vnode.text = ""; } vnode.elm = api.createComment(vnode.text!); } else if (sel !== undefined) { // 如果選擇器不為空 // 解析選擇器 // Parse selector const hashIdx = sel.indexOf("#"); const dotIdx = sel.indexOf(".", hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; const elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag, data) : api.createElement(tag, data)); if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); // 執行模塊的create鉤子函數 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); // 如果vnode中有子節點,創建子Vnode對應的DOM元素并追加到DOM樹上 if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)); } } } else if (is.primitive(vnode.text)) { // 如果vode的text值是string/number,創建文本節點并追加到DOM樹 api.appendChild(elm, api.createTextNode(vnode.text)); } const hook = vnode.data!.hook; if (isDef(hook)) { // 執行傳入的鉤子 create hook.create?.(emptyNode, vnode); if (hook.insert) { insertedVnodeQueue.push(vnode); } } } else { // 如果選擇器為空,創建文本節點 vnode.elm = api.createTextNode(vnode.text!); } // 返回新創建的DOM return vnode.elm; } function addVnodes( parentElm: Node, before: Node | null, vnodes: VNode[], startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } } function invokeDestroyHook(vnode: VNode) { const data = vnode.data; if (data !== undefined) { // 執行的destroy 鉤子函數 data?.hook?.destroy?.(vnode); // 調用模塊的destroy鉤子函數 for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); // 執行子節點的destroy鉤子函數 if (vnode.children !== undefined) { for (let j = 0; j < vnode.children.length; ++j) { const child = vnode.children[j]; if (child != null && typeof child !== "string") { invokeDestroyHook(child); } } } } } function removeVnodes( parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number ): void { for (; startIdx <= endIdx; ++startIdx) { let listeners: number; let rm: () => void; const ch = vnodes[startIdx]; if (ch != null) { // 如果sel 有值 if (isDef(ch.sel)) { invokeDestroyHook(ch); // 防止重復調用 listeners = cbs.remove.length + 1; // 創建刪除的回調函數 rm = createRmCb(ch.elm!, listeners); for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 執行remove鉤子函數 const removeHook = ch?.data?.hook?.remove; if (isDef(removeHook)) { removeHook(ch, rm); } else { // 如果沒有鉤子函數,直接調用刪除元素的方法 rm(); } } else { // Text node // 如果是文本節點,直接是調用刪除元素的方法 api.removeChild(parentElm, ch.elm!); } } } } 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); } } } /* 對比兩個新舊節點,然后找到差異并更新DOM 第一部分 1.觸發prepatch鉤子函數 2.觸發update鉤子函數 第二部分 1.新節點有text屬性,且不等于舊節點的text屬性 -》如果舊節點有children,移除舊節點children對應的DOM元素;設置新節點對應的DOM元素的textContent 2.新舊節點都有children,且不相等-》調用updateChildren();對比子節點,并且更新子節點的差異 3.只有新節點有children屬性-》如果舊節點有text屬性,清空對應DOM元素的textContent;添加所有的子節點 4.只有舊節點有children屬性-》移除所有舊節點 5.只有舊節點有text屬性=》清空對應的DOM元素的textContent 第三部分 1.觸發postpatch鉤子函數 */ function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { const hook = vnode.data?.hook; // 首先執行prepatch鉤子函數 hook?.prepatch?.(oldVnode, vnode); const elm = (vnode.elm = oldVnode.elm)!; const oldCh = oldVnode.children as VNode[]; const ch = vnode.children as VNode[]; // 如果新舊vnode相同返回 if (oldVnode === vnode) return; if (vnode.data !== undefined) { // 執行模塊的update鉤子函數 for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // 執行update鉤子函數 vnode.data.hook?.update?.(oldVnode, vnode); } // 如果是vnode.text 未定義 if (isUndef(vnode.text)) { // 如果是新舊節點都有 children if (isDef(oldCh) && isDef(ch)) { // 使用diff算法對比子節點,更新子節點 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 如果新節點有children,舊節點沒有children // 如果舊節點有text,清空dom 元素的內容 if (isDef(oldVnode.text)) api.setTextContent(elm, ""); // 批量添加子節點 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 如果舊節點有children,新節點沒有children // 批量移除子節點 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { // 如果舊節點有 text,清空 DOM 元素 api.setTextContent(elm, ""); } } else if (oldVnode.text !== vnode.text) { // 如果沒有設置 vnode.text if (isDef(oldCh)) { // 如果舊節點有children,移除 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } // 設置 DOM 元素的textContent為 vnode.text api.setTextContent(elm, vnode.text!); } // 最后執行postpatch鉤子函數 hook?.postpatch?.(oldVnode, vnode); } // init 內部返回 patch 函數,把vnode渲染成真實dom,并返回vnode return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 保存新插入的節點的隊列,為了觸發鉤子函數 const insertedVnodeQueue: VNodeQueue = []; // 執行模塊的pre 鉤子函數 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 如果oldVnode不是Vnode,創建Vnode并設置elm if (!isVnode(oldVnode)) { // 把Dom元素轉化成空的Vnode oldVnode = emptyNodeAt(oldVnode); } // 如果新舊節點是相同節點 if (sameVnode(oldVnode, vnode)) { // 找節點的差異并更新DOM,這里的原理就是diff算法 patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 如果新舊節點不同,vnode創建對應的DOM // 獲取當前的DOM元素 elm = oldVnode.elm!; // 獲取父元素 parent = api.parentNode(elm) as Node; // 創建Vnode對應的DOM元素,并觸發init/create 鉤子函數 createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 如果父元素不為空,把vnode對應的DOM插入到父元素中 api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); // 移除舊節點 removeVnodes(parent, [oldVnode], 0, 0); } } // 執行insert 鉤子函數 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); } // 執行模塊的post 鉤子函數 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 返回vnode 作為下次更新的舊節點 return vnode; }; }
接下來,我們分開介紹init方法中的內容。
初始化的時候,將每個 modules 下的相應的鉤子都追加都一個數組里面
在進行 patch 的各個階段,觸發對應的鉤子去處理對應的事情
這種方式比較方便擴展。新增鉤子的時候,不需要更改到主要的流程
這些模塊的鉤子,主要用在更新節點的時候,會在不同的生命周期里面去觸發對應的鉤子,從而更新這些模塊。
let i: number; let j: number; const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [], }; // 把傳入的所有模塊的鉤子函數,統一存儲到cbs對象中 // 最終構建的cbs對象的形式cbs = {create:[fn1,fn2],update:[],....} for (i = 0; i < hooks.length; ++i) { // cbs.create= [], cbs.update = []... cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { // modules 傳入的模塊數組 // 獲取模塊中的hook函數 // hook = modules[0][create]... const hook = modules[j][hooks[i]]; if (hook !== undefined) { // 把獲取到的hook函數放入到cbs 對應的鉤子函數數組中 (cbs[hooks[i]] as any[]).push(hook); } } }
init 方法最后返回一個 patch 方法 。
主要的邏輯如下 :
觸發 pre 鉤子
如果舊節點非 vnode, 則新創建空的 vnode
新舊節點為 sameVnode 的話,則調用 patchVnode 更新 vnode , 否則創建新節點
觸發收集到的新元素 insert 鉤子
觸發 post 鉤子
// init 內部返回 patch 函數,把vnode渲染成真實dom,并返回vnode return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 保存新插入的節點的隊列,為了觸發鉤子函數 const insertedVnodeQueue: VNodeQueue = []; // 執行模塊的pre 鉤子函數 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 如果oldVnode不是Vnode,創建Vnode并設置elm if (!isVnode(oldVnode)) { // 把Dom元素轉化成空的Vnode oldVnode = emptyNodeAt(oldVnode); } // 如果新舊節點是相同節點 if (sameVnode(oldVnode, vnode)) { // 找節點的差異并更新DOM,這里的原理就是diff算法 patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 如果新舊節點不同,vnode創建對應的DOM // 獲取當前的DOM元素 elm = oldVnode.elm!; // 獲取父元素 parent = api.parentNode(elm) as Node; // 創建Vnode對應的DOM元素,并觸發init/create 鉤子函數 createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 如果父元素不為空,把vnode對應的DOM插入到父元素中 api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); // 移除舊節點 removeVnodes(parent, [oldVnode], 0, 0); } } // 執行insert 鉤子函數 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); } // 執行模塊的post 鉤子函數 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 返回vnode 作為下次更新的舊節點 return vnode; };
patchVnode方法
主要的邏輯如下 :
觸發 prepatch 鉤子
觸發 update 鉤子, 這里主要為了更新對應的 module 內容
非文本節點的情況 , 調用 updateChildren 更新所有子節點
文本節點的情況 , 直接 api.setTextContent(elm, vnode.text as string)
這里在對比的時候,就會直接更新元素內容了。并不會等到對比完才更新 DOM 元素。
/* 對比兩個新舊節點,然后找到差異并更新DOM 第一部分 1.觸發prepatch鉤子函數 2.觸發update鉤子函數 第二部分 1.新節點有text屬性,且不等于舊節點的text屬性 -》如果舊節點有children,移除舊節點children對應的DOM元素;設置新節點對應的DOM元素的textContent 2.新舊節點都有children,且不相等-》調用updateChildren();對比子節點,并且更新子節點的差異 3.只有新節點有children屬性-》如果舊節點有text屬性,清空對應DOM元素的textContent;添加所有的子節點 4.只有舊節點有children屬性-》移除所有舊節點 5.只有舊節點有text屬性=》清空對應的DOM元素的textContent 第三部分 1.觸發postpatch鉤子函數 */ function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { const hook = vnode.data?.hook; // 首先執行prepatch鉤子函數 hook?.prepatch?.(oldVnode, vnode); const elm = (vnode.elm = oldVnode.elm)!; const oldCh = oldVnode.children as VNode[]; const ch = vnode.children as VNode[]; // 如果新舊vnode相同返回 if (oldVnode === vnode) return; if (vnode.data !== undefined) { // 執行模塊的update鉤子函數 for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // 執行update鉤子函數 vnode.data.hook?.update?.(oldVnode, vnode); } // 如果是vnode.text 未定義 if (isUndef(vnode.text)) { // 如果是新舊節點都有 children if (isDef(oldCh) && isDef(ch)) { // 使用diff算法對比子節點,更新子節點 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 如果新節點有children,舊節點沒有children // 如果舊節點有text,清空dom 元素的內容 if (isDef(oldVnode.text)) api.setTextContent(elm, ""); // 批量添加子節點 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 如果舊節點有children,新節點沒有children // 批量移除子節點 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { // 如果舊節點有 text,清空 DOM 元素 api.setTextContent(elm, ""); } } else if (oldVnode.text !== vnode.text) { // 如果沒有設置 vnode.text if (isDef(oldCh)) { // 如果舊節點有children,移除 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } // 設置 DOM 元素的textContent為 vnode.text api.setTextContent(elm, vnode.text!); } // 最后執行postpatch鉤子函數 hook?.postpatch?.(oldVnode, vnode); }
patchVnode 里面最重要的方法,也是整個 diff 里面的最核心方法。
主要的邏輯如下:
優先處理特殊場景,先對比兩端。也就是
舊 vnode 頭 vs 新 vnode 頭
舊 vnode 尾 vs 新 vnode 尾
舊 vnode 頭 vs 新 vnode 尾
舊 vnode 尾 vs 新 vnode 頭
首尾不一樣的情況,尋找 key 相同的節點,找不到則新建元素
如果找到 key,但是,元素選擇器變化了,也新建元素
如果找到 key,并且元素選擇沒變, 則移動元素
兩個列表對比完之后,清理多余的元素,新增添加的元素
不提供 key 的情況下,如果只是順序改變的情況,例如第一個移動到末尾。這個時候,會導致其實更新了后面的所有元素。
// 更新子節點 function updateChildren( parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue ) { let oldStartIdx = 0, 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: any; 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 as Node, api.nextSibling(oldEndVnode.elm as Node) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 最右側對比最左側 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 移動元素到左側指針的后面 api.insertBefore( parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node ); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 首尾都不一樣的情況,尋找相同 key 的節點,所以使用的時候加上key可以調高效率 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 如果找不到 key 對應的元素,就新建元素 api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); newStartVnode = newCh[++newStartIdx]; } else { // 如果找到 key 對應的元素,就移動元素 elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); } else { patchVnode( elmToMove, newStartVnode, insertedVnodeQueue ); oldCh[idxInOld] = undefined as any; api.insertBefore( parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node ); } 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); } } }
主要功能就是添加 Vnodes 到 真實 DOM 中。
function addVnodes( parentElm: Node, before: Node | null, vnodes: VNode[], startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } }
主要邏輯如下:
循環觸發 destroy 鉤子,遞歸觸發子節點的鉤子
觸發 remove 鉤子,利用 createRmCb , 在所有監聽器執行后,才調用 api.removeChild,刪除真正的 DOM 節點
function invokeDestroyHook(vnode: VNode) { const data = vnode.data; if (data !== undefined) { // 執行的destroy 鉤子函數 data?.hook?.destroy?.(vnode); // 調用模塊的destroy鉤子函數 for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); // 執行子節點的destroy鉤子函數 if (vnode.children !== undefined) { for (let j = 0; j < vnode.children.length; ++j) { const child = vnode.children[j]; if (child != null && typeof child !== "string") { invokeDestroyHook(child); } } } } }
//創建一個刪除的回調,多次調用這個回調,直到監聽器都沒了,就刪除元素 function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm) as Node; api.removeChild(parent, childElm); } }; }
function removeVnodes( parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number ): void { for (; startIdx <= endIdx; ++startIdx) { let listeners: number; let rm: () => void; const ch = vnodes[startIdx]; if (ch != null) { // 如果sel 有值 if (isDef(ch.sel)) { invokeDestroyHook(ch); // 防止重復調用 listeners = cbs.remove.length + 1; // 創建刪除的回調函數 rm = createRmCb(ch.elm!, listeners); for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 執行remove鉤子函數 const removeHook = ch?.data?.hook?.remove; if (isDef(removeHook)) { removeHook(ch, rm); } else { // 如果沒有鉤子函數,直接調用刪除元素的方法 rm(); } } else { // Text node // 如果是文本節點,直接是調用刪除元素的方法 api.removeChild(parentElm, ch.elm!); } } } }
將 vnode 轉換成真正的 DOM 元素。
主要邏輯如下:
觸發 init 鉤子
處理注釋節點
創建元素并設置 id , class
觸發模塊 create 鉤子 。
處理子節點
處理文本節點
觸發 vnodeData 的 create 鉤子
/* 1.觸發鉤子函數init 2.把vnode轉換為DOM對象,存儲到vnode.elm中 - sel是!--》創建注釋節點 - sel不為空 --》創建對應的DOM對象;觸發模塊的鉤子函數create;創建所有子節點對應的DOM對象;觸發鉤子函數create;如果是vnode有inset鉤子函數,追加到隊列 - sel為空 --》創建文本節點 3.返回vnode.elm */ function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any; let data = vnode.data; if (data !== undefined) { // 執行init鉤子函數 const init = data.hook?.init; if (isDef(init)) { init(vnode); data = vnode.data; } } // 把vnode轉換成真實dom對象(沒有渲染到頁面) const children = vnode.children; const sel = vnode.sel; if (sel === "!") { // 如果選擇器是!,創建注釋節點 if (isUndef(vnode.text)) { vnode.text = ""; } vnode.elm = api.createComment(vnode.text!); } else if (sel !== undefined) { // 如果選擇器不為空 // 解析選擇器 // Parse selector const hashIdx = sel.indexOf("#"); const dotIdx = sel.indexOf(".", hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; const elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag, data) : api.createElement(tag, data)); if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); // 執行模塊的create鉤子函數 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); // 如果vnode中有子節點,創建子Vnode對應的DOM元素并追加到DOM樹上 if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)); } } } else if (is.primitive(vnode.text)) { // 如果vode的text值是string/number,創建文本節點并追加到DOM樹 api.appendChild(elm, api.createTextNode(vnode.text)); } const hook = vnode.data!.hook; if (isDef(hook)) { // 執行傳入的鉤子 create hook.create?.(emptyNode, vnode); if (hook.insert) { insertedVnodeQueue.push(vnode); } } } else { // 如果選擇器為空,創建文本節點 vnode.elm = api.createTextNode(vnode.text!); } // 返回新創建的DOM return vnode.elm; }
到此,關于“Virtual DOM作用是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。