溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入剖析:Vue核心之虛擬DOM

發(fā)布時(shí)間:2020-07-13 15:51:10 來源:網(wǎng)絡(luò) 閱讀:266 作者:wx5d61fdc401976 欄目:開發(fā)技術(shù)

前言
使用 Vue 做項(xiàng)目也有兩年時(shí)間了,對(duì) Vue 的 api也用的比較得心應(yīng)手了,雖然對(duì) Vue 的一些實(shí)現(xiàn)原理也耳有所聞,例如 虛擬DOM、flow、數(shù)據(jù)驅(qū)動(dòng)、路由原理等等,但是自己并沒有特意去探究這些原理的基礎(chǔ)以及 Vue 源碼是如何利用這些原理進(jìn)行框架實(shí)現(xiàn)的,所以利用空閑時(shí)間,進(jìn)行 Vue 框架相關(guān)技術(shù)原理和 Vue 框架的具體實(shí)現(xiàn)的整理。如果你對(duì) Vue 的實(shí)現(xiàn)原理很感興趣,那么就可以開始這系列文章的閱讀,將會(huì)為你打開 Vue 的底層世界大門,對(duì)它的實(shí)現(xiàn)細(xì)節(jié)一探究竟。 本文為 Virtual DOM的技術(shù)原理和 Vue 框架的具體實(shí)現(xiàn)。

辛苦編寫良久,還望手動(dòng)點(diǎn)贊鼓勵(lì)~

github地址為:github.com/fengshi123/…,上面匯總了作者所有的博客文章,如果喜歡或者有所啟發(fā),請(qǐng)幫忙給個(gè) star ~,對(duì)作者也是一種鼓勵(lì)。

一、真實(shí)DOM和其解析流程
? 本節(jié)我們主要介紹真實(shí) DOM 的解析過程,通過介紹其解析過程以及存在的問題,從而引出為什么需要虛擬DOM。一圖勝千言,如下圖為 webkit 渲染引擎工作流程圖

? 所有的瀏覽器渲染引擎工作流程大致分為5步:創(chuàng)建 DOM 樹 —> 創(chuàng)建 Style Rules -> 構(gòu)建 Render 樹 —> 布局 Layout -—> 繪制 Painting。

第一步,構(gòu)建 DOM 樹:用 HTML 分析器,分析 HTML 元素,構(gòu)建一棵 DOM 樹;
第二步,生成樣式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 樣式,生成頁(yè)面的樣式表;
第三步,構(gòu)建 Render 樹:將 DOM 樹和樣式表關(guān)聯(lián)起來,構(gòu)建一棵 Render 樹(Attachment)。每個(gè) DOM 節(jié)點(diǎn)都有 attach 方法,接受樣式信息,返回一個(gè) render 對(duì)象(又名 renderer),這些 render 對(duì)象最終會(huì)被構(gòu)建成一棵 Render 樹;
第四步,確定節(jié)點(diǎn)坐標(biāo):根據(jù) Render 樹結(jié)構(gòu),為每個(gè) Render 樹上的節(jié)點(diǎn)確定一個(gè)在顯示屏上出現(xiàn)的精確坐標(biāo);
第五步,繪制頁(yè)面:根據(jù) Render 樹和節(jié)點(diǎn)顯示坐標(biāo),然后調(diào)用每個(gè)節(jié)點(diǎn)的 paint 方法,將它們繪制出來。
注意點(diǎn):

1、DOM 樹的構(gòu)建是文檔加載完成開始的? 構(gòu)建 DOM 樹是一個(gè)漸進(jìn)過程,為達(dá)到更好的用戶體驗(yàn),渲染引擎會(huì)盡快將內(nèi)容顯示在屏幕上,它不必等到整個(gè) HTML 文檔解析完成之后才開始構(gòu)建 render 樹和布局。

2、Render 樹是 DOM 樹和 CSS 樣式表構(gòu)建完畢后才開始構(gòu)建的? 這三個(gè)過程在實(shí)際進(jìn)行的時(shí)候并不是完全獨(dú)立的,而是會(huì)有交叉,會(huì)一邊加載,一邊解析,以及一邊渲染。

3、CSS 的解析注意點(diǎn)? CSS 的解析是從右往左逆向解析的,嵌套標(biāo)簽越多,解析越慢。

4、JS 操作真實(shí) DOM 的代價(jià)? 用我們傳統(tǒng)的開發(fā)模式,原生 JS 或 JQ 操作 DOM 時(shí),瀏覽器會(huì)從構(gòu)建 DOM 樹開始從頭到尾執(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ì)算為無用功。計(jì)算 DOM 節(jié)點(diǎn)坐標(biāo)值等都是白白浪費(fèi)的性能。即使計(jì)算機(jī)硬件一直在迭代更新,操作 DOM 的代價(jià)仍舊是昂貴的,頻繁操作還是會(huì)出現(xiàn)頁(yè)面卡頓,影響用戶體驗(yàn)

二、Virtual-DOM 基礎(chǔ)
2.1、虛擬 DOM 的好處
? 虛擬 DOM 就是為了解決瀏覽器性能問題而被設(shè)計(jì)出來的。如前,若一次操作中有 10 次更新 DOM 的動(dòng)作,虛擬 DOM 不會(huì)立即操作 DOM,而是將這 10 次更新的 diff 內(nèi)容保存到本地一個(gè) JS 對(duì)象中,最終將這個(gè) JS 對(duì)象一次性 attch 到 DOM 樹上,再進(jìn)行后續(xù)操作,避免大量無謂的計(jì)算量。所以,用 JS 對(duì)象模擬 DOM 節(jié)點(diǎn)的好處是,頁(yè)面的更新可以先全部反映在 JS 對(duì)象(虛擬 DOM )上,操作內(nèi)存中的 JS 對(duì)象的速度顯然要更快,等更新完成后,再將最終的 JS 對(duì)象映射成真實(shí)的 DOM,交由瀏覽器去繪制。

2.2、算法實(shí)現(xiàn)
2.2.1、用 JS 對(duì)象模擬 DOM 樹
(1)如何用 JS 對(duì)象模擬 DOM 樹

例如一個(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>
復(fù)制代碼
我們用 JavaScript 對(duì)象來表示 DOM 節(jié)點(diǎn),使用對(duì)象的屬性記錄節(jié)點(diǎn)的類型、屬性、子節(jié)點(diǎn)等。

element.js 中表示節(jié)點(diǎn)對(duì)象代碼如下:

/**

  • Element virdual-dom 對(duì)象定義
  • @param {String} tagName - dom 元素名稱
  • @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;
復(fù)制代碼
根據(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'])
])
復(fù)制代碼
現(xiàn)在 ul 就是我們用 JavaScript 對(duì)象表示的 DOM 結(jié)構(gòu),我們輸出查看 ul 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)如下:

(2)渲染用 JS 表示的 DOM 對(duì)象

但是頁(yè)面上并沒有這個(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
    }
    復(fù)制代碼
    我們通過查看以上 render 方法,會(huì)根據(jù) tagName 構(gòu)建一個(gè)真正的 DOM 節(jié)點(diǎn),然后設(shè)置這個(gè)節(jié)點(diǎn)的屬性,最后遞歸地把自己的子節(jié)點(diǎn)也構(gòu)建起來。

我們將構(gòu)建好的 DOM 結(jié)構(gòu)添加到頁(yè)面 body 上面,如下:

ulRoot = ul.render();
document.body.appendChild(ulRoot);
復(fù)制代碼
這樣,頁(yè)面 body 里面就有真正的 DOM 結(jié)構(gòu),效果如下圖所示:

2.2.2、比較兩棵虛擬 DOM 樹的差異 — diff 算法
diff 算法用來比較兩棵 Virtual DOM 樹的差異,如果需要兩棵樹的完全比較,那么 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ì)新舊兩棵樹進(jìn)行一個(gè)深度優(yōu)先的遍歷,這樣每個(gè)節(jié)點(diǎn)都會(huì)有一個(gè)唯一的標(biāo)記:

dfs-walk
在深度優(yōu)先遍歷的時(shí)候,每遍歷到一個(gè)節(jié)點(diǎn)就把該節(jié)點(diǎn)和新的的樹進(jìn)行對(duì)比。如果有差異的話就記錄到一個(gè)對(duì)象里面。

// diff 函數(shù),對(duì)比兩棵樹
function diff(oldTree, newTree) {
var index = 0 // 當(dāng)前節(jié)點(diǎn)的標(biāo)志
var patches = {} // 用來記錄每個(gè)節(jié)點(diǎn)差異的對(duì)象
dfsWalk(oldTree, newTree, index, patches)
return patches
}

// 對(duì)兩棵樹進(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
}
}
復(fù)制代碼
從以上可以得出,patches[1] 表示 p ,patches[3] 表示 ul ,以此類推。

(2)差異類型

DOM 操作導(dǎo)致的差異類型包括以下幾種:

節(jié)點(diǎn)替換:節(jié)點(diǎn)改變了,例如將上面的 div 換成 h2;
順序互換:移動(dòng)、刪除、新增子節(jié)點(diǎn),例如上面 div 的子節(jié)點(diǎn),把 p 和 ul 順序互換;
屬性更改:修改了節(jié)點(diǎn)的屬性,例如把上面 li 的 class 樣式類刪除;
文本改變:改變文本節(jié)點(diǎn)的文本內(nèi)容,例如將上面 p 節(jié)點(diǎn)的文本內(nèi)容更改為 “Real Dom”;
以上描述的幾種差異類型在代碼中定義如下所示:

var REPLACE = 0 // 替換原先的節(jié)點(diǎn)
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了節(jié)點(diǎn)的屬性
var TEXT = 3 // 文本內(nèi)容改變
復(fù)制代碼
(3)列表對(duì)比算法

? 子節(jié)點(diǎn)的對(duì)比算法,例如 p, ul, div 的順序換成了 div, p, ul。這個(gè)該怎么對(duì)比?如果按照同層級(jí)進(jìn)行順序?qū)Ρ鹊脑?,它們都?huì)被替換掉。如 p 和 div 的 tagName 不同,p 會(huì)被 div 所替代。最終,三個(gè)節(jié)點(diǎn)都會(huì)被替換,這樣 DOM 開銷就非常大。而實(shí)際上是不需要替換節(jié)點(diǎn),而只需要經(jīng)過節(jié)點(diǎn)移動(dòng)就可以達(dá)到,我們只需知道怎么進(jìn)行移動(dòng)。

? 將這個(gè)問題抽象出來其實(shí)就是字符串的最小編輯距離問題(Edition Distance),最常見的解決方法是 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 也被稱為編輯距離(Edit Distance),通過動(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ì)算過程如下:

本文的 demo 使用插件 list-diff2 算法進(jìn)行比較,該算法的時(shí)間復(fù)雜度偉 O(n*m),雖然該算法并非最優(yōu)的算法,但是用于對(duì)于 dom 元素的常規(guī)操作是足夠的。該算法具體的實(shí)現(xiàn)過程這里不再詳細(xì)介紹,該算法的具體介紹可以參照:github.com/livoras/lis…

(4)實(shí)例輸出

兩個(gè)虛擬 DOM 對(duì)象如下圖所示,其中 ul1 表示原有的虛擬 DOM 樹,ul2 表示改變后的虛擬 DOM 樹

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);
復(fù)制代碼
我們查看輸出的兩個(gè)虛擬 DOM 對(duì)象之間的差異對(duì)象如下圖所示,我們能通過差異對(duì)象得到,兩個(gè)虛擬 DOM 對(duì)象之間進(jìn)行了哪些變化,從而根據(jù)這個(gè)差異對(duì)象(patches)更改原先的真實(shí) DOM 結(jié)構(gòu),從而將頁(yè)面的 DOM 結(jié)構(gòu)進(jìn)行更改。

2.2.3、將兩個(gè)虛擬 DOM 對(duì)象的差異應(yīng)用到真正的 DOM 樹
(1)深度優(yōu)先遍歷 DOM 樹

? 因?yàn)椴襟E一所構(gòu)建的 JavaScript 對(duì)象樹和 render 出來真正的 DOM 樹的信息、結(jié)構(gòu)是一樣的。所以我們可以對(duì)那棵 DOM 樹也進(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)
}
}
復(fù)制代碼
(2)對(duì)原有 DOM 樹進(jìn)行 DOM 操作

我們根據(jù)不同類型的差異對(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)
}
})
}
復(fù)制代碼
(3)DOM結(jié)構(gòu)改變

通過將第 2.2.2 得到的兩個(gè) DOM 對(duì)象之間的差異,應(yīng)用到第一個(gè)(原先)DOM 結(jié)構(gòu)中,我們可以看到 DOM 結(jié)構(gòu)進(jìn)行了預(yù)期的變化,如下圖所示:

2.3、結(jié)語
相關(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è)步驟來實(shí)現(xiàn):

用 JS 對(duì)象模擬 DOM 樹 — 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>
復(fù)制代碼
比較兩棵虛擬 DOM 樹的差異 — diff.js

將兩個(gè)虛擬 DOM 對(duì)象的差異應(yīng)用到真正的 DOM 樹 — 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)
}
})
}
復(fù)制代碼
三、Vue 源碼 Virtual-DOM 簡(jiǎn)析
我們從第二章節(jié)(Virtual-DOM 基礎(chǔ))中已經(jīng)掌握 Virtual DOM 渲染成真實(shí)的 DOM 實(shí)際上要經(jīng)歷 VNode 的定義、diff、patch 等過程,所以本章節(jié) Vue 源碼的解析也按這幾個(gè)過程來簡(jiǎn)析。

3.1、VNode 模擬 DOM 樹
3.1.1、VNode 類簡(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ù) 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;
Context: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for 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
}
}
復(fù)制代碼
這里千萬不要因?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過程中可以提高diff的效率
3.1.2、源碼創(chuàng)建 VNode 過程
(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)
}
復(fù)制代碼
通過查看 Vue 的 function,我們知道 Vue 只能通過 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)
}

}
復(fù)制代碼
(2)Vue 實(shí)例掛載

Vue 中是通過 $mount 實(shí)例方法去掛載 dom 的,下面我們通過分析 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ù)制代碼
我們發(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ù)制代碼
我們發(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
}
復(fù)制代碼
從上面的代碼可以看到,mountComponent 核心就是先實(shí)例化一個(gè)渲染W(wǎng)atcher,在它的回調(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è)私有方法,它用來把實(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 方法來返回 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
}
復(fù)制代碼
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> {

// 省略一系列非主線代碼

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()
}
}
復(fù)制代碼
_createElement 方法有 5 個(gè)參數(shù),context 表示 VNode 的上下文環(huán)境,它是 Component 類型;tag表示標(biāo)簽,它可以是一個(gè)字符串,也可以是一個(gè) Component;data 表示 VNode 的數(shù)據(jù),它是一個(gè) VNodeData 類型,可以在 flow/vnode.js 中找到它的定義;children 表示當(dāng)前 VNode 的子節(jié)點(diǎn),它是任意類型的,需要被規(guī)范為標(biāo)準(zhǔn)的 VNode 數(shù)組;

3.1.3、實(shí)例查看
為了更直觀查看我們平時(shí)寫的 Vue 代碼如何用 VNode 類來表示,我們通過一個(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!'
}
})
復(fù)制代碼
我們打印出其對(duì)應(yīng)的 VNode 表示:

3.2、diff 過程
3.2.1、Vue.js 源碼的 diff 調(diào)用邏輯
Vue.js 源碼實(shí)例化了一個(gè) watcher,這個(gè) ~ 被添加到了在模板當(dāng)中所綁定變量的依賴當(dāng)中,一旦 model 中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化,這些響應(yīng)式的數(shù)據(jù)所維護(hù)的 dep 數(shù)組便會(huì)調(diào)用 dep.notify() 方法完成所有依賴遍歷執(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
}
復(fù)制代碼
完成視圖的更新工作事實(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
}
}
復(fù)制代碼
在這個(gè)方法當(dāng)中最為關(guān)鍵的就是 vm.patch 方法,這也是整個(gè) virtual-dom 當(dāng)中最為核心的方法,主要完成了prevVnode 和 vnode 的 diff 過程并根據(jù)需要操作的 vdom 節(jié)點(diǎn)打 patch,最后生成新的真實(shí) dom 節(jié)點(diǎn)并完成視圖的更新工作。

接下來,讓我們看下 vm.patch的邏輯過程, 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)
}
......
}
}
復(fù)制代碼
在 patch 方法中,我們看到會(huì)分為兩種情況,一種是當(dāng) oldVnode 不存在時(shí),會(huì)創(chuàng)建新的節(jié)點(diǎn);另一種則是已經(jīng)存在 oldVnode ,那么會(huì)對(duì) oldVnode 和 vnode 進(jìn)行 diff 及 patch 的過程。其中 patch 過程中會(huì)調(diào)用 sameVnode 方法來對(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ì)直接跳過 diff 的過程,進(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)
)
}
復(fù)制代碼
diff 過程中主要是通過調(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沒有文本節(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沒有,那么就清空這個(gè)節(jié)點(diǎn)
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點(diǎn)的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
......
}
復(fù)制代碼
從以上代碼得知,

diff 過程中又分了好幾種情況,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 沒有文本節(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 沒有,那么就清空這個(gè)文本節(jié)點(diǎn)。
3.2.2、子節(jié)點(diǎn) diff 流程分析
(1)Vue.js 源碼

? 這里著重分析下updateChildren方法,它也是整個(gè) diff 過程中最重要的環(huán)節(jié),以下為 Vue.js 的源碼過程,為了更形象理解 diff 過程,我們給出相關(guān)的示意圖來講解。

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)
}

}
復(fù)制代碼
在開始遍歷 diff 前,首先給 oldCh和 newCh 分別分配一個(gè) startIndex 和 endIndex 來作為遍歷的索引,當(dāng)oldCh 或者 newCh 遍歷完后(遍歷完的條件就是 oldCh 或者 newCh 的 startIndex >= endIndex ),就停止oldCh 和 newCh 的 diff 過程。接下來通過實(shí)例來看下整個(gè) diff 的過程(節(jié)點(diǎn)屬性中不帶 key 的情況)。

(2)無 key 的 diff 過程

我們通過以下示意圖對(duì)以上代碼過程進(jìn)行講解:

(2.1)首先從第一個(gè)節(jié)點(diǎn)開始比較,不管是 oldCh 還是 newCh 的起始或者終止節(jié)點(diǎn)都不存在 sameVnode ,同時(shí)節(jié)點(diǎn)屬性中是不帶 key標(biāo)記的,因此第一輪的 diff 完后,newCh的 startVnode 被添加到 oldStartVnode的前面,同時(shí) newStartIndex前移一位;

?????????è?°
(2.2)第二輪的 diff中,滿足 sameVnode(oldStartVnode, newStartVnode),因此對(duì)這2個(gè) vnode 進(jìn)行diff,最后將 patch 打到 oldStartVnode 上,同時(shí) oldStartVnode和 newStartIndex 都向前移動(dòng)一位 ;

?????????è?°
(2.3)第三輪的 diff 中,滿足 sameVnode(oldEndVnode, newStartVnode),那么首先對(duì) oldEndVnode和newStartVnode 進(jìn)行 diff,并對(duì) oldEndVnode進(jìn)行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex前移一位,oldStartVnode 后移一位;

?????????è?°
(2.4)第四輪的 diff中,過程同步驟3;

?????????è?°
(2.5)第五輪的 diff 中,同過程1;

?????????è?°
(2.6)遍歷的過程結(jié)束后,newStartIdx > newEndIdx,說明此時(shí) oldCh 存在多余的節(jié)點(diǎn),那么最后就需要將這些多余的節(jié)點(diǎn)刪除。

?????????è?°
(3)有 key 的 diff 流程

在 vnode 不帶 key 的情況下,每一輪的 diff 過程當(dāng)中都是起始和結(jié)束節(jié)點(diǎn)進(jìn)行比較,直到 oldCh 或者newCh 被遍歷完。而當(dāng)為 vnode 引入 key 屬性后,在每一輪的 diff 過程中,當(dāng)起始和結(jié)束節(jié)點(diǎn)都沒有找到sameVnode 時(shí),然后再判斷在 newStartVnode 的屬性中是否有 key,且是否在 oldKeyToIndx 中找到對(duì)應(yīng)的節(jié)點(diǎn) :

如果不存在這個(gè) key,那么就將這個(gè) newStartVnode作為新的節(jié)點(diǎn)創(chuàng)建且插入到原有的 root 的子節(jié)點(diǎn)中;
如果存在這個(gè) key,那么就取出 oldCh 中的存在這個(gè) key 的 vnode,然后再進(jìn)行 diff 的過;
通過以上分析,給vdom上添加 key屬性后,遍歷 diff 的過程中,當(dāng)起始點(diǎn),結(jié)束點(diǎn)的搜尋及 diff 出現(xiàn)還是無法匹配的情況下時(shí),就會(huì)用 key 來作為唯一標(biāo)識(shí),來進(jìn)行 diff,這樣就可以提高 diff 效率。

帶有 Key屬性的 vnode的 diff 過程可見下圖:

(3.1)首先從第一個(gè)節(jié)點(diǎn)開始比較,不管是 oldCh 還是 newCh 的起始或者終止節(jié)點(diǎn)都不存在 sameVnode,但節(jié)點(diǎn)屬性中是帶 key 標(biāo)記的, 然后在 oldKeyToIndx 中找到對(duì)應(yīng)的節(jié)點(diǎn),這樣第一輪 diff 過后 oldCh 上的B節(jié)點(diǎn)被刪除了,但是 newCh 上的B節(jié)點(diǎn)上 elm 屬性保持對(duì) oldCh 上 B節(jié)點(diǎn) 的elm引用。

?????????è?°
(3.2)第二輪的 diff 中,滿足 sameVnode(oldStartVnode, newStartVnode),因此對(duì)這2個(gè) vnode 進(jìn)行diff,最后將 patch 打到 oldStartVnode上,同時(shí) oldStartVnode 和 newStartIndex 都向前移動(dòng)一位 ;

?????????è?°
(3.3)第三輪的 diff中,滿足 sameVnode(oldEndVnode, newStartVnode),那么首先對(duì) oldEndVnode 和newStartVnode 進(jìn)行 diff,并對(duì) oldEndVnode 進(jìn)行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex 前移一位,oldStartVnode后移一位;

?????????è?°
(3.4)第四輪的diff中,過程同步驟2;

?????????è?°
(3.5)第五輪的diff中,因?yàn)榇藭r(shí) oldStartIndex 已經(jīng)大于 oldEndIndex,所以將剩余的 Vnode 隊(duì)列插入隊(duì)列最后。

?????????è?°
3.3、patch 過程
通過3.2章節(jié)介紹的 diff 過程中,我們會(huì)看到 nodeOps 相關(guān)的方法對(duì)真實(shí) DOM 結(jié)構(gòu)進(jìn)行操作,nodeOps 定義在 src/platforms/web/runtime/node-ops.js 中,其為基本 DOM 操作,這里就不在詳細(xì)介紹。

export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
return document.createTextNode(text)
}

export function createComment (text: string): Comment {
return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
復(fù)制代碼
3.4、總結(jié)
通過前三小節(jié)簡(jiǎn)析,我們從主線上把模板和數(shù)據(jù)如何渲染成最終的 DOM 的過程分析完畢了,我們可以通過下圖更直觀地看到從初始化 Vue 到最終渲染的整個(gè)過程。

四、總結(jié)
本文從通過介紹真實(shí) DOM 結(jié)構(gòu)其解析過程以及存在的問題,從而引出為什么需要虛擬 DOM;然后分析虛擬DOM 的好處,以及其一些理論基礎(chǔ)和基礎(chǔ)算法的實(shí)現(xiàn);最后根據(jù)我們已經(jīng)掌握的基礎(chǔ)知識(shí),再一步步去查看Vue.js 的源碼如何實(shí)現(xiàn)的。從存在問題 —> 理論基礎(chǔ) —> 具體實(shí)踐,一步步深入,幫助大家更好的了解什么是Virtual DOM、為什么需要 Virtual DOM、以及 Virtual DOM的具體實(shí)現(xiàn),希望本文對(duì)您有幫助。

辛苦編寫良久,如果對(duì)你有幫助,還望手動(dòng)點(diǎn)贊鼓勵(lì)~~

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI