這篇文章主要介紹了Vue中的虛擬DOM如何構(gòu)建的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Vue中的虛擬DOM如何構(gòu)建文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。
梅州ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書(shū)未來(lái)市場(chǎng)廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書(shū)銷(xiāo)售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話(huà)聯(lián)系或者加微信:13518219792(備注:SSL證書(shū)合作)期待與您的合作!
虛擬DOM技術(shù)使得我們的頁(yè)面渲染的效率更高,減輕了節(jié)點(diǎn)的操作從而提高性能。
DOM
和其解析流程 本節(jié)我們主要介紹真實(shí) DOM
的解析過(guò)程,通過(guò)介紹其解析過(guò)程以及存在的問(wèn)題,從而引出為什么需要虛擬DOM
。一圖勝千言,如下圖為 webkit
渲染引擎工作流程圖
所有的瀏覽器渲染引擎工作流程大致分為5步:創(chuàng)建 DOM
樹(shù) —> 創(chuàng)建 Style Rules
-> 構(gòu)建 Render
樹(shù) —> 布局 Layout
-—> 繪制 Painting
。
第一步,構(gòu)建 DOM 樹(shù):用 HTML 分析器,分析 HTML 元素,構(gòu)建一棵 DOM 樹(shù);
第二步,生成樣式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 樣式,生成頁(yè)面的樣式表;
第三步,構(gòu)建 Render 樹(shù):將 DOM 樹(shù)和樣式表關(guān)聯(lián)起來(lái),構(gòu)建一棵 Render 樹(shù)(Attachment)。每個(gè) DOM 節(jié)點(diǎn)都有 attach 方法,接受樣式信息,返回一個(gè) render 對(duì)象(又名 renderer),這些 render 對(duì)象最終會(huì)被構(gòu)建成一棵 Render 樹(shù);
第四步,確定節(jié)點(diǎn)坐標(biāo):根據(jù) Render 樹(shù)結(jié)構(gòu),為每個(gè) Render 樹(shù)上的節(jié)點(diǎn)確定一個(gè)在顯示屏上出現(xiàn)的精確坐標(biāo);
第五步,繪制頁(yè)面:根據(jù) Render 樹(shù)和節(jié)點(diǎn)顯示坐標(biāo),然后調(diào)用每個(gè)節(jié)點(diǎn)的 paint 方法,將它們繪制出來(lái)。
注意點(diǎn):
1、DOM
樹(shù)的構(gòu)建是文檔加載完成開(kāi)始的?構(gòu)建 DOM
樹(shù)是一個(gè)漸進(jìn)過(guò)程,為達(dá)到更好的用戶(hù)體驗(yàn),渲染引擎會(huì)盡快將內(nèi)容顯示在屏幕上,它不必等到整個(gè) HTML
文檔解析完成之后才開(kāi)始構(gòu)建 render
樹(shù)和布局。
2、Render
樹(shù)是 DOM
樹(shù)和 CSS
樣式表構(gòu)建完畢后才開(kāi)始構(gòu)建的?這三個(gè)過(guò)程在實(shí)際進(jìn)行的時(shí)候并不是完全獨(dú)立的,而是會(huì)有交叉,會(huì)一邊加載,一邊解析,以及一邊渲染。
3、CSS
的解析注意點(diǎn)?CSS
的解析是從右往左逆向解析的,嵌套標(biāo)簽越多,解析越慢。
4、JS
操作真實(shí) DOM
的代價(jià)?用我們傳統(tǒng)的開(kāi)發(fā)模式,原生 JS
或 JQ
操作 DOM
時(shí),瀏覽器會(huì)從構(gòu)建 DOM 樹(shù)開(kāi)始從頭到尾執(zhí)行一遍流程。在一次操作中,我需要更新 10 個(gè) DOM
節(jié)點(diǎn),瀏覽器收到第一個(gè) DOM
請(qǐng)求后并不知道還有 9 次更新操作,因此會(huì)馬上執(zhí)行流程,最終執(zhí)行10 次。例如,第一次計(jì)算完,緊接著下一個(gè) DOM
更新請(qǐng)求,這個(gè)節(jié)點(diǎn)的坐標(biāo)值就變了,前一次計(jì)算為無(wú)用功。計(jì)算 DOM
節(jié)點(diǎn)坐標(biāo)值等都是白白浪費(fèi)的性能。即使計(jì)算機(jī)硬件一直在迭代更新,操作 DOM
的代價(jià)仍舊是昂貴的,頻繁操作還是會(huì)出現(xiàn)頁(yè)面卡頓,影響用戶(hù)體驗(yàn)
Virtual-DOM
基礎(chǔ)DOM
的好處 虛擬 DOM
就是為了解決瀏覽器性能問(wèn)題而被設(shè)計(jì)出來(lái)的。如前,若一次操作中有 10 次更新 DOM
的動(dòng)作,虛擬 DOM
不會(huì)立即操作 DOM
,而是將這 10 次更新的 diff
內(nèi)容保存到本地一個(gè) JS
對(duì)象中,最終將這個(gè) JS
對(duì)象一次性 attch
到 DOM
樹(shù)上,再進(jìn)行后續(xù)操作,避免大量無(wú)謂的計(jì)算量。所以,用 JS
對(duì)象模擬 DOM
節(jié)點(diǎn)的好處是,頁(yè)面的更新可以先全部反映在 JS
對(duì)象(虛擬 DOM
)上,操作內(nèi)存中的 JS
對(duì)象的速度顯然要更快,等更新完成后,再將最終的 JS
對(duì)象映射成真實(shí)的 DOM
,交由瀏覽器去繪制。
JS
對(duì)象模擬 DOM
樹(shù)(1)如何用 JS
對(duì)象模擬 DOM
樹(shù)
例如一個(gè)真實(shí)的 DOM
節(jié)點(diǎn)如下:
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div>
我們用 JavaScript
對(duì)象來(lái)表示 DOM
節(jié)點(diǎn),使用對(duì)象的屬性記錄節(jié)點(diǎn)的類(lèi)型、屬性、子節(jié)點(diǎn)等。
element.js
中表示節(jié)點(diǎn)對(duì)象代碼如下:
/** * Element virdual-dom 對(duì)象定義 * @param {String} tagName - dom 元素名稱(chēng) * @param {Object} props - dom 屬性 * @param {Array<Element|String>} - 子節(jié)點(diǎn) */ function Element(tagName, props, children) { this.tagName = tagName this.props = props this.children = children // dom 元素的 key 值,用作唯一標(biāo)識(shí)符 if(props.key){ this.key = props.key } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素個(gè)數(shù) this.count = count } function createElement(tagName, props, children){ return new Element(tagName, props, children); } module.exports = createElement;
根據(jù) element
對(duì)象的設(shè)定,則上面的 DOM
結(jié)構(gòu)就可以簡(jiǎn)單表示為:
var el = require("./element.js"); var ul = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div',{},['Hello World']) ])
現(xiàn)在 ul
就是我們用 JavaScript
對(duì)象表示的 DOM
結(jié)構(gòu),我們輸出查看 ul
對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下:
(2)渲染用 JS
表示的 DOM
對(duì)象
但是頁(yè)面上并沒(méi)有這個(gè)結(jié)構(gòu),下一步我們介紹如何將 ul
渲染成頁(yè)面上真實(shí)的 DOM
結(jié)構(gòu),相關(guān)渲染函數(shù)如下:
/** * render 將virdual-dom 對(duì)象渲染為實(shí)際 DOM 元素 */ Element.prototype.render = function () { var el = document.createElement(this.tagName) var props = this.props // 設(shè)置節(jié)點(diǎn)的DOM屬性 for (var propName in props) { var propValue = props[propName] el.setAttribute(propName, propValue) } var children = this.children || [] children.forEach(function (child) { var childEl = (child instanceof Element) ? child.render() // 如果子節(jié)點(diǎn)也是虛擬DOM,遞歸構(gòu)建DOM節(jié)點(diǎn) : document.createTextNode(child) // 如果字符串,只構(gòu)建文本節(jié)點(diǎn) el.appendChild(childEl) }) return el }
我們通過(guò)查看以上 render
方法,會(huì)根據(jù) tagName
構(gòu)建一個(gè)真正的 DOM
節(jié)點(diǎn),然后設(shè)置這個(gè)節(jié)點(diǎn)的屬性,最后遞歸地把自己的子節(jié)點(diǎn)也構(gòu)建起來(lái)。
我們將構(gòu)建好的 DOM
結(jié)構(gòu)添加到頁(yè)面 body
上面,如下:
ulRoot = ul.render(); document.body.appendChild(ulRoot);
這樣,頁(yè)面 body
里面就有真正的 DOM
結(jié)構(gòu),效果如下圖所示:
DOM
樹(shù)的差異 — diff
算法diff
算法用來(lái)比較兩棵 Virtual DOM
樹(shù)的差異,如果需要兩棵樹(shù)的完全比較,那么 diff
算法的時(shí)間復(fù)雜度為O(n^3)
。但是在前端當(dāng)中,你很少會(huì)跨越層級(jí)地移動(dòng) DOM
元素,所以 Virtual DOM
只會(huì)對(duì)同一個(gè)層級(jí)的元素進(jìn)行對(duì)比,如下圖所示, div
只會(huì)和同一層級(jí)的 div
對(duì)比,第二層級(jí)的只會(huì)跟第二層級(jí)對(duì)比,這樣算法復(fù)雜度就可以達(dá)到 O(n)
。
(1)深度優(yōu)先遍歷,記錄差異
在實(shí)際的代碼中,會(huì)對(duì)新舊兩棵樹(shù)進(jìn)行一個(gè)深度優(yōu)先的遍歷,這樣每個(gè)節(jié)點(diǎn)都會(huì)有一個(gè)唯一的標(biāo)記:
在深度優(yōu)先遍歷的時(shí)候,每遍歷到一個(gè)節(jié)點(diǎn)就把該節(jié)點(diǎn)和新的的樹(shù)進(jìn)行對(duì)比。如果有差異的話(huà)就記錄到一個(gè)對(duì)象里面。
// diff 函數(shù),對(duì)比兩棵樹(shù) function diff(oldTree, newTree) { var index = 0 // 當(dāng)前節(jié)點(diǎn)的標(biāo)志 var patches = {} // 用來(lái)記錄每個(gè)節(jié)點(diǎn)差異的對(duì)象 dfsWalk(oldTree, newTree, index, patches) return patches } // 對(duì)兩棵樹(shù)進(jìn)行深度優(yōu)先遍歷 function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (typeof (oldNode) === "string" && typeof (newNode) === "string") { // 文本內(nèi)容改變 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 節(jié)點(diǎn)相同,比較屬性 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 比較子節(jié)點(diǎn),如果子節(jié)點(diǎn)有'ignore'屬性,則不需要比較 if (!isIgnoreChildren(newNode)) { diffChildren( oldNode.children, newNode.children, index, patches, currentPatch ) } } else if(newNode !== null){ // 新節(jié)點(diǎn)和舊節(jié)點(diǎn)不同,用 replace 替換 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } }
從以上可以得出,patches[1]
表示 p
,patches[3]
表示 ul
,以此類(lèi)推。
(2)差異類(lèi)型
DOM
操作導(dǎo)致的差異類(lèi)型包括以下幾種:
節(jié)點(diǎn)替換:節(jié)點(diǎn)改變了,例如將上面的 div
換成 h2
;
順序互換:移動(dòng)、刪除、新增子節(jié)點(diǎn),例如上面 div
的子節(jié)點(diǎn),把 p
和 ul
順序互換;
屬性更改:修改了節(jié)點(diǎn)的屬性,例如把上面 li
的 class
樣式類(lèi)刪除;
文本改變:改變文本節(jié)點(diǎn)的文本內(nèi)容,例如將上面 p
節(jié)點(diǎn)的文本內(nèi)容更改為 “Real Dom
”;
以上描述的幾種差異類(lèi)型在代碼中定義如下所示:
var REPLACE = 0 // 替換原先的節(jié)點(diǎn) var REORDER = 1 // 重新排序 var PROPS = 2 // 修改了節(jié)點(diǎn)的屬性 var TEXT = 3 // 文本內(nèi)容改變
(3)列表對(duì)比算法
子節(jié)點(diǎn)的對(duì)比算法,例如 p, ul, div
的順序換成了 div, p, ul
。這個(gè)該怎么對(duì)比?如果按照同層級(jí)進(jìn)行順序?qū)Ρ鹊脑?huà),它們都會(huì)被替換掉。如 p
和 div
的 tagName
不同,p
會(huì)被 div
所替代。最終,三個(gè)節(jié)點(diǎn)都會(huì)被替換,這樣 DOM
開(kāi)銷(xiāo)就非常大。而實(shí)際上是不需要替換節(jié)點(diǎn),而只需要經(jīng)過(guò)節(jié)點(diǎn)移動(dòng)就可以達(dá)到,我們只需知道怎么進(jìn)行移動(dòng)。
將這個(gè)問(wèn)題抽象出來(lái)其實(shí)就是字符串的最小編輯距離問(wèn)題(Edition Distance
),最常見(jiàn)的解決方法是 Levenshtein Distance
, Levenshtein Distance
是一個(gè)度量?jī)蓚€(gè)字符序列之間差異的字符串度量標(biāo)準(zhǔn),兩個(gè)單詞之間的 Levenshtein Distance
是將一個(gè)單詞轉(zhuǎn)換為另一個(gè)單詞所需的單字符編輯(插入、刪除或替換)的最小數(shù)量。Levenshtein Distance
是1965年由蘇聯(lián)數(shù)學(xué)家 Vladimir Levenshtein 發(fā)明的。Levenshtein Distance
也被稱(chēng)為編輯距離(Edit Distance
),通過(guò)動(dòng)態(tài)規(guī)劃求解,時(shí)間復(fù)雜度為 O(M*N)
。
定義:對(duì)于兩個(gè)字符串 a、b
,則他們的 Levenshtein Distance
為:
示例:字符串 a
和 b
,a=“abcde” ,b=“cabef”
,根據(jù)上面給出的計(jì)算公式,則他們的 Levenshtein Distance
的計(jì)算過(guò)程如下:
本文的 demo
使用插件 list-diff2
算法進(jìn)行比較,該算法的時(shí)間復(fù)雜度偉 O(n*m)
,雖然該算法并非最優(yōu)的算法,但是用于對(duì)于 dom
元素的常規(guī)操作是足夠的。該算法具體的實(shí)現(xiàn)過(guò)程這里不再詳細(xì)介紹,該算法的具體介紹可以參照:github.com/livoras/lis…
(4)實(shí)例輸出
兩個(gè)虛擬 DOM
對(duì)象如下圖所示,其中 ul1
表示原有的虛擬 DOM
樹(shù),ul2
表示改變后的虛擬 DOM
樹(shù)
var ul1 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div',{},['Hello World']) ]) var ul2 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 21']), el('li', { class: 'item' }, ['Item 23']) ]), el('p',{},['Hello World']) ]) var patches = diff(ul1,ul2); console.log('patches:',patches);
我們查看輸出的兩個(gè)虛擬 DOM
對(duì)象之間的差異對(duì)象如下圖所示,我們能通過(guò)差異對(duì)象得到,兩個(gè)虛擬 DOM
對(duì)象之間進(jìn)行了哪些變化,從而根據(jù)這個(gè)差異對(duì)象(patches
)更改原先的真實(shí) DOM
結(jié)構(gòu),從而將頁(yè)面的 DOM
結(jié)構(gòu)進(jìn)行更改。
DOM
對(duì)象的差異應(yīng)用到真正的 DOM
樹(shù)(1)深度優(yōu)先遍歷 DOM
樹(shù)
因?yàn)椴襟E一所構(gòu)建的 JavaScript
對(duì)象樹(shù)和 render
出來(lái)真正的 DOM
樹(shù)的信息、結(jié)構(gòu)是一樣的。所以我們可以對(duì)那棵 DOM
樹(shù)也進(jìn)行深度優(yōu)先的遍歷,遍歷的時(shí)候從步驟二生成的 patches
對(duì)象中找出當(dāng)前遍歷的節(jié)點(diǎn)差異,如下相關(guān)代碼所示:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { // 從patches拿出當(dāng)前節(jié)點(diǎn)的差異 var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 深度遍歷子節(jié)點(diǎn) for (var i = 0; i < len; i++) { var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } // 對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行DOM操作 if (currentPatches) { applyPatches(node, currentPatches) } }
(2)對(duì)原有 DOM
樹(shù)進(jìn)行 DOM
操作
我們根據(jù)不同類(lèi)型的差異對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行不同的 DOM
操作 ,例如如果進(jìn)行了節(jié)點(diǎn)替換,就進(jìn)行節(jié)點(diǎn)替換 DOM
操作;如果節(jié)點(diǎn)文本發(fā)生了改變,則進(jìn)行文本替換的 DOM
操作;以及子節(jié)點(diǎn)重排、屬性改變等 DOM
操作,相關(guān)代碼如 applyPatches
所示 :
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
(3)DOM結(jié)構(gòu)改變
通過(guò)將第 2.2.2 得到的兩個(gè) DOM
對(duì)象之間的差異,應(yīng)用到第一個(gè)(原先)DOM
結(jié)構(gòu)中,我們可以看到 DOM
結(jié)構(gòu)進(jìn)行了預(yù)期的變化,如下圖所示:
相關(guān)代碼實(shí)現(xiàn)已經(jīng)放到 github 上面,有興趣的同學(xué)可以clone運(yùn)行實(shí)驗(yàn),github地址為:github.com/fengshi123/…
Virtual DOM
算法主要實(shí)現(xiàn)上面三個(gè)步驟來(lái)實(shí)現(xiàn):
用 JS
對(duì)象模擬 DOM
樹(shù) — element.js
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div>
比較兩棵虛擬 DOM
樹(shù)的差異 — diff.js
將兩個(gè)虛擬 DOM
對(duì)象的差異應(yīng)用到真正的 DOM
樹(shù) — patch.js
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
Vue
源碼 Virtual-DOM
簡(jiǎn)析我們從第二章節(jié)(Virtual-DOM
基礎(chǔ))中已經(jīng)掌握 Virtual DOM
渲染成真實(shí)的 DOM
實(shí)際上要經(jīng)歷 VNode
的定義、diff
、patch
等過(guò)程,所以本章節(jié) Vue
源碼的解析也按這幾個(gè)過(guò)程來(lái)簡(jiǎn)析。
VNode
模擬 DOM
樹(shù)VNode
類(lèi)簡(jiǎn)析在 Vue.js
中,Virtual DOM
是用 VNode
這個(gè) Class
去描述,它定義在 src/core/vdom/vnode.js
中 ,從以下代碼塊中可以看到 Vue.js
中的 Virtual DOM
的定義較為復(fù)雜一些,因?yàn)樗@里包含了很多 Vue.js
的特性。實(shí)際上 Vue.js
中 Virtual DOM
是借鑒了一個(gè)開(kāi)源庫(kù) snabbdom 的實(shí)現(xiàn),然后加入了一些 Vue.js
的一些特性。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support 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 } }
這里千萬(wàn)不要因?yàn)?VNode
的這么屬性而被嚇到,或者咬緊牙去摸清楚每個(gè)屬性的意義,其實(shí),我們主要了解其幾個(gè)核心的關(guān)鍵屬性就差不多了,例如:
tag
屬性即這個(gè)vnode
的標(biāo)簽屬性
data
屬性包含了最后渲染成真實(shí)dom
節(jié)點(diǎn)后,節(jié)點(diǎn)上的class
,attribute
,style
以及綁定的事件
children
屬性是vnode
的子節(jié)點(diǎn)
text
屬性是文本屬性
elm
屬性為這個(gè)vnode
對(duì)應(yīng)的真實(shí)dom
節(jié)點(diǎn)
key
屬性是vnode
的標(biāo)記,在diff
過(guò)程中可以提高diff
的效率
VNode
過(guò)程(1)初始化vue
我們?cè)趯?shí)例化一個(gè) vue
實(shí)例,也即 new Vue( )
時(shí),實(shí)際上是執(zhí)行 src/core/instance/index.js
中定義的 Function
函數(shù)。
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
通過(guò)查看 Vue
的 function
,我們知道 Vue
只能通過(guò) new
關(guān)鍵字初始化,然后調(diào)用 this._init
方法,該方法在 src/core/instance/init.js
中定義。
Vue.prototype._init = function (options?: Object) { const vm: Component = this // 省略一系列其它初始化的代碼 if (vm.$options.el) { console.log('vm.$options.el:',vm.$options.el); vm.$mount(vm.$options.el) } }
(2)Vue
實(shí)例掛載
Vue
中是通過(guò) $mount
實(shí)例方法去掛載 dom
的,下面我們通過(guò)分析 compiler
版本的 mount
實(shí)現(xiàn),相關(guān)源碼在目錄 src/platforms/web/entry-runtime-with-compiler.js
文件中定義:。
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // 省略一系列初始化以及邏輯判斷代碼 return mount.call(this, el, hydrating) }
我們發(fā)現(xiàn)最終還是調(diào)用用原先原型上的 $mount
方法掛載 ,原先原型上的 $mount
方法在 src/platforms/web/runtime/index.js
中定義 。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
我們發(fā)現(xiàn)$mount
方法實(shí)際上會(huì)去調(diào)用 mountComponent
方法,這個(gè)方法定義在 src/core/instance/lifecycle.js
文件中
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 省略一系列其它代碼 let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // 生成虛擬 vnode const vnode = vm._render() // 更新 DOM vm._update(vnode, hydrating) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 實(shí)例化一個(gè)渲染W(wǎng)atcher,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
從上面的代碼可以看到,mountComponent
核心就是先實(shí)例化一個(gè)渲染Watcher
,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent
方法,在此方法中調(diào)用 vm._render
方法先生成虛擬 Node,最終調(diào)用 vm._update
更新 DOM
。
(3)創(chuàng)建虛擬 Node
Vue
的 _render
方法是實(shí)例的一個(gè)私有方法,它用來(lái)把實(shí)例渲染成一個(gè)虛擬 Node
。它的定義在 src/core/instance/render.js
文件中:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options let vnode try { // 省略一系列代碼 currentRenderingInstance = vm // 調(diào)用 createElement 方法來(lái)返回 vnode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`){} } // set parent vnode.parent = _parentVnode console.log("vnode...:",vnode); return vnode }
Vue.js
利用 _createElement
方法創(chuàng)建 VNode
,它定義在 src/core/vdom/create-elemenet.js
中:
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // 省略一系列非主線(xiàn)代碼 if (normalizationType === ALWAYS_NORMALIZE) { // 場(chǎng)景是 render 函數(shù)不是編譯生成的 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { // 場(chǎng)景是 render 函數(shù)是編譯生成的 children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // 創(chuàng)建虛擬 vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }
_createElement
方法有 5 個(gè)參數(shù),context
表示 VNode 的上下文環(huán)境,它是 Component
類(lèi)型;tag
表示標(biāo)簽,它可以是一個(gè)字符串,也可以是一個(gè) Component
;data
表示 VNode 的數(shù)據(jù),它是一個(gè) VNodeData
類(lèi)型,可以在 flow/vnode.js
中找到它的定義;children
表示當(dāng)前 VNode 的子節(jié)點(diǎn),它是任意類(lèi)型的,需要被規(guī)范為標(biāo)準(zhǔn)的 VNode
數(shù)組;
為了更直觀查看我們平時(shí)寫(xiě)的 Vue
代碼如何用 VNode
類(lèi)來(lái)表示,我們通過(guò)一個(gè)實(shí)例的轉(zhuǎn)換進(jìn)行更深刻了解。
例如,實(shí)例化一個(gè) Vue
實(shí)例:
var app = new Vue({ el: '#app', render: function (createElement) { return createElement('div', { attrs: { id: 'app', class: "class_box" }, }, this.message) }, data: { message: 'Hello Vue!' } })
我們打印出其對(duì)應(yīng)的 VNode
表示:
diff
過(guò)程Vue.js
源碼的 diff
調(diào)用邏輯Vue.js
源碼實(shí)例化了一個(gè) watcher
,這個(gè) ~ 被添加到了在模板當(dāng)中所綁定變量的依賴(lài)當(dāng)中,一旦 model
中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化,這些響應(yīng)式的數(shù)據(jù)所維護(hù)的 dep
數(shù)組便會(huì)調(diào)用 dep.notify()
方法完成所有依賴(lài)遍歷執(zhí)行的工作,這包括視圖的更新,即 updateComponent
方法的調(diào)用。watcher
和 updateComponent
方法定義在 src/core/instance/lifecycle.js
文件中 。
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 省略一系列其它代碼 let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // 生成虛擬 vnode const vnode = vm._render() // 更新 DOM vm._update(vnode, hydrating) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 實(shí)例化一個(gè)渲染W(wǎng)atcher,在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
完成視圖的更新工作事實(shí)上就是調(diào)用了vm._update
方法,這個(gè)方法接收的第一個(gè)參數(shù)是剛生成的Vnode
,調(diào)用的vm._update
方法定義在 src/core/instance/lifecycle.js
中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode if (!prevVnode) { // 第一個(gè)參數(shù)為真實(shí)的node節(jié)點(diǎn),則為初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 如果需要diff的prevVnode存在,那么對(duì)prevVnode和vnode進(jìn)行diff vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } }
在這個(gè)方法當(dāng)中最為關(guān)鍵的就是 vm.__patch__
方法,這也是整個(gè) virtual-dom
當(dāng)中最為核心的方法,主要完成了prevVnode
和 vnode
的 diff
過(guò)程并根據(jù)需要操作的 vdom
節(jié)點(diǎn)打 patch
,最后生成新的真實(shí) dom
節(jié)點(diǎn)并完成視圖的更新工作。
接下來(lái),讓我們看下 vm.__patch__
的邏輯過(guò)程, vm.__patch__
方法定義在 src/core/vdom/patch.js
中。
function patch (oldVnode, vnode, hydrating, removeOnly) { ...... if (isUndef(oldVnode)) { // 當(dāng)oldVnode不存在時(shí),創(chuàng)建新的節(jié)點(diǎn) isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // 對(duì)oldVnode和vnode進(jìn)行diff,并對(duì)oldVnode打patch const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } ...... } }
在 patch
方法中,我們看到會(huì)分為兩種情況,一種是當(dāng) oldVnode
不存在時(shí),會(huì)創(chuàng)建新的節(jié)點(diǎn);另一種則是已經(jīng)存在 oldVnode
,那么會(huì)對(duì) oldVnode
和 vnode
進(jìn)行 diff
及 patch
的過(guò)程。其中 patch
過(guò)程中會(huì)調(diào)用 sameVnode
方法來(lái)對(duì)對(duì)傳入的2個(gè) vnode
進(jìn)行基本屬性的比較,只有當(dāng)基本屬性相同的情況下才認(rèn)為這個(gè)2個(gè)vnode
只是局部發(fā)生了更新,然后才會(huì)對(duì)這2個(gè) vnode
進(jìn)行 diff
,如果2個(gè) vnode
的基本屬性存在不一致的情況,那么就會(huì)直接跳過(guò) diff
的過(guò)程,進(jìn)而依據(jù) vnode
新建一個(gè)真實(shí)的 dom
,同時(shí)刪除老的 dom
節(jié)點(diǎn)。
function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) }
diff
過(guò)程中主要是通過(guò)調(diào)用 patchVnode
方法進(jìn)行的:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { ...... const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children // 如果vnode沒(méi)有文本節(jié)點(diǎn) if (isUndef(vnode.text)) { // 如果oldVnode的children屬性存在且vnode的children屬性也存在 if (isDef(oldCh) && isDef(ch)) { // updateChildren,對(duì)子節(jié)點(diǎn)進(jìn)行diff if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } // 如果oldVnode的text存在,那么首先清空text的內(nèi)容,然后將vnode的children添加進(jìn)去 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 刪除elm下的oldchildren removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // oldVnode有子節(jié)點(diǎn),而vnode沒(méi)有,那么就清空這個(gè)節(jié)點(diǎn) nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點(diǎn)的文本元素 nodeOps.setTextContent(elm, vnode.text) } ...... }
從以上代碼得知,
diff
過(guò)程中又分了好幾種情況,oldCh
為 oldVnode
的子節(jié)點(diǎn),ch
為 Vnode
的子節(jié)點(diǎn):
首先進(jìn)行文本節(jié)點(diǎn)的判斷,若 oldVnode.text !== vnode.text
,那么就會(huì)直接進(jìn)行文本節(jié)點(diǎn)的替換;
在vnode
沒(méi)有文本節(jié)點(diǎn)的情況下,進(jìn)入子節(jié)點(diǎn)的 diff
;
當(dāng) oldCh
和 ch
都存在且不相同的情況下,調(diào)用 updateChildren
對(duì)子節(jié)點(diǎn)進(jìn)行 diff
;
若 oldCh
不存在,ch
存在,首先清空 oldVnode
的文本節(jié)點(diǎn),同時(shí)調(diào)用 addVnodes
方法將 ch
添加到elm
真實(shí) dom
節(jié)點(diǎn)當(dāng)中;
若 oldCh
存在,ch
不存在,則刪除 elm
真實(shí)節(jié)點(diǎn)下的 oldCh
子節(jié)點(diǎn);
若 oldVnode
有文本節(jié)點(diǎn),而 vnode
沒(méi)有,那么就清空這個(gè)文本節(jié)點(diǎn)。
diff
流程分析(1)Vue.js
源碼
這里著重分析下updateChildren
方法,它也是整個(gè) diff
過(guò)程中最重要的環(huán)節(jié),以下為 Vue.js
的源碼過(guò)程,為了更形象理解 diff
過(guò)程,我們給出相關(guān)的示意圖來(lái)講解。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 為oldCh和newCh分別建立索引,為之后遍歷的依據(jù) 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, idxInOld, vnodeToMove, refElm // 直到oldCh或者newCh被遍歷完后跳出循環(huán) while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
在開(kāi)始遍歷 diff
前,首先給 oldCh
和 newCh
分別分配一個(gè) startIndex
和 endIndex
來(lái)作為遍歷的索引,當(dāng)oldCh
或者 newCh
遍歷完后(遍歷完的條件就是 oldCh
或者 newCh
的
標(biāo)題名稱(chēng):Vue中的虛擬DOM如何構(gòu)建
網(wǎng)址分享:http://aaarwkj.com/article6/ggooog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁(yè)設(shè)計(jì)公司、品牌網(wǎng)站設(shè)計(jì)、ChatGPT、響應(yīng)式網(wǎng)站、電子商務(wù)、品牌網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)