您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue中的虛擬DOM如何構建的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Vue中的虛擬DOM如何構建文章都會有所收獲,下面我們一起來看看吧。
虛擬DOM技術使得我們的頁面渲染的效率更高,減輕了節點的操作從而提高性能。
DOM
和其解析流程 本節我們主要介紹真實 DOM
的解析過程,通過介紹其解析過程以及存在的問題,從而引出為什么需要虛擬DOM
。一圖勝千言,如下圖為 webkit
渲染引擎工作流程圖
所有的瀏覽器渲染引擎工作流程大致分為5步:創建 DOM
樹 —> 創建 Style Rules
-> 構建 Render
樹 —> 布局 Layout
-—> 繪制 Painting
。
第一步,構建 DOM 樹:用 HTML 分析器,分析 HTML 元素,構建一棵 DOM 樹;
第二步,生成樣式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 樣式,生成頁面的樣式表;
第三步,構建 Render 樹:將 DOM 樹和樣式表關聯起來,構建一棵 Render 樹(Attachment)。每個 DOM 節點都有 attach 方法,接受樣式信息,返回一個 render 對象(又名 renderer),這些 render 對象最終會被構建成一棵 Render 樹;
第四步,確定節點坐標:根據 Render 樹結構,為每個 Render 樹上的節點確定一個在顯示屏上出現的精確坐標;
第五步,繪制頁面:根據 Render 樹和節點顯示坐標,然后調用每個節點的 paint 方法,將它們繪制出來。
注意點:
1、DOM
樹的構建是文檔加載完成開始的? 構建 DOM
樹是一個漸進過程,為達到更好的用戶體驗,渲染引擎會盡快將內容顯示在屏幕上,它不必等到整個 HTML
文檔解析完成之后才開始構建 render
樹和布局。
2、Render
樹是 DOM
樹和 CSS
樣式表構建完畢后才開始構建的? 這三個過程在實際進行的時候并不是完全獨立的,而是會有交叉,會一邊加載,一邊解析,以及一邊渲染。
3、CSS
的解析注意點? CSS
的解析是從右往左逆向解析的,嵌套標簽越多,解析越慢。
4、JS
操作真實 DOM
的代價? 用我們傳統的開發模式,原生 JS
或 JQ
操作 DOM
時,瀏覽器會從構建 DOM 樹開始從頭到尾執行一遍流程。在一次操作中,我需要更新 10 個 DOM
節點,瀏覽器收到第一個 DOM
請求后并不知道還有 9 次更新操作,因此會馬上執行流程,最終執行10 次。例如,第一次計算完,緊接著下一個 DOM
更新請求,這個節點的坐標值就變了,前一次計算為無用功。計算 DOM
節點坐標值等都是白白浪費的性能。即使計算機硬件一直在迭代更新,操作 DOM
的代價仍舊是昂貴的,頻繁操作還是會出現頁面卡頓,影響用戶體驗
Virtual-DOM
基礎DOM
的好處 虛擬 DOM
就是為了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有 10 次更新 DOM
的動作,虛擬 DOM
不會立即操作 DOM
,而是將這 10 次更新的 diff
內容保存到本地一個 JS
對象中,最終將這個 JS
對象一次性 attch
到 DOM
樹上,再進行后續操作,避免大量無謂的計算量。所以,用 JS
對象模擬 DOM
節點的好處是,頁面的更新可以先全部反映在 JS
對象(虛擬 DOM
)上,操作內存中的 JS
對象的速度顯然要更快,等更新完成后,再將最終的 JS
對象映射成真實的 DOM
,交由瀏覽器去繪制。
JS
對象模擬 DOM
樹(1)如何用 JS
對象模擬 DOM
樹
例如一個真實的 DOM
節點如下:
<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
對象來表示 DOM
節點,使用對象的屬性記錄節點的類型、屬性、子節點等。
element.js
中表示節點對象代碼如下:
/** * Element virdual-dom 對象定義 * @param {String} tagName - dom 元素名稱 * @param {Object} props - dom 屬性 * @param {Array<Element|String>} - 子節點 */ function Element(tagName, props, children) { this.tagName = tagName this.props = props this.children = children // dom 元素的 key 值,用作唯一標識符 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++ }) // 子元素個數 this.count = count } function createElement(tagName, props, children){ return new Element(tagName, props, children); } module.exports = createElement;
根據 element
對象的設定,則上面的 DOM
結構就可以簡單表示為:
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']) ])
現在 ul
就是我們用 JavaScript
對象表示的 DOM
結構,我們輸出查看 ul
對應的數據結構如下:
(2)渲染用 JS
表示的 DOM
對象
但是頁面上并沒有這個結構,下一步我們介紹如何將 ul
渲染成頁面上真實的 DOM
結構,相關渲染函數如下:
/** * render 將virdual-dom 對象渲染為實際 DOM 元素 */ Element.prototype.render = function () { var el = document.createElement(this.tagName) var props = this.props // 設置節點的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() // 如果子節點也是虛擬DOM,遞歸構建DOM節點 : document.createTextNode(child) // 如果字符串,只構建文本節點 el.appendChild(childEl) }) return el }
我們通過查看以上 render
方法,會根據 tagName
構建一個真正的 DOM
節點,然后設置這個節點的屬性,最后遞歸地把自己的子節點也構建起來。
我們將構建好的 DOM
結構添加到頁面 body
上面,如下:
ulRoot = ul.render(); document.body.appendChild(ulRoot);
這樣,頁面 body
里面就有真正的 DOM
結構,效果如下圖所示:
DOM
樹的差異 — diff
算法diff
算法用來比較兩棵 Virtual DOM
樹的差異,如果需要兩棵樹的完全比較,那么 diff
算法的時間復雜度為O(n^3)
。但是在前端當中,你很少會跨越層級地移動 DOM
元素,所以 Virtual DOM
只會對同一個層級的元素進行對比,如下圖所示, div
只會和同一層級的 div
對比,第二層級的只會跟第二層級對比,這樣算法復雜度就可以達到 O(n)
。
(1)深度優先遍歷,記錄差異
在實際的代碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每個節點都會有一個唯一的標記:
在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的的樹進行對比。如果有差異的話就記錄到一個對象里面。
// diff 函數,對比兩棵樹 function diff(oldTree, newTree) { var index = 0 // 當前節點的標志 var patches = {} // 用來記錄每個節點差異的對象 dfsWalk(oldTree, newTree, index, patches) return patches } // 對兩棵樹進行深度優先遍歷 function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (typeof (oldNode) === "string" && typeof (newNode) === "string") { // 文本內容改變 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 節點相同,比較屬性 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 比較子節點,如果子節點有'ignore'屬性,則不需要比較 if (!isIgnoreChildren(newNode)) { diffChildren( oldNode.children, newNode.children, index, patches, currentPatch ) } } else if(newNode !== null){ // 新節點和舊節點不同,用 replace 替換 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } }
從以上可以得出,patches[1]
表示 p
,patches[3]
表示 ul
,以此類推。
(2)差異類型
DOM
操作導致的差異類型包括以下幾種:
節點替換:節點改變了,例如將上面的 div
換成 h2
;
順序互換:移動、刪除、新增子節點,例如上面 div
的子節點,把 p
和 ul
順序互換;
屬性更改:修改了節點的屬性,例如把上面 li
的 class
樣式類刪除;
文本改變:改變文本節點的文本內容,例如將上面 p
節點的文本內容更改為 “Real Dom
”;
以上描述的幾種差異類型在代碼中定義如下所示:
var REPLACE = 0 // 替換原先的節點 var REORDER = 1 // 重新排序 var PROPS = 2 // 修改了節點的屬性 var TEXT = 3 // 文本內容改變
(3)列表對比算法
子節點的對比算法,例如 p, ul, div
的順序換成了 div, p, ul
。這個該怎么對比?如果按照同層級進行順序對比的話,它們都會被替換掉。如 p
和 div
的 tagName
不同,p
會被 div
所替代。最終,三個節點都會被替換,這樣 DOM
開銷就非常大。而實際上是不需要替換節點,而只需要經過節點移動就可以達到,我們只需知道怎么進行移動。
將這個問題抽象出來其實就是字符串的最小編輯距離問題(Edition Distance
),最常見的解決方法是 Levenshtein Distance
, Levenshtein Distance
是一個度量兩個字符序列之間差異的字符串度量標準,兩個單詞之間的 Levenshtein Distance
是將一個單詞轉換為另一個單詞所需的單字符編輯(插入、刪除或替換)的最小數量。Levenshtein Distance
是1965年由蘇聯數學家 Vladimir Levenshtein 發明的。Levenshtein Distance
也被稱為編輯距離(Edit Distance
),通過動態規劃求解,時間復雜度為 O(M*N)
。
定義:對于兩個字符串 a、b
,則他們的 Levenshtein Distance
為:
示例:字符串 a
和 b
,a=“abcde” ,b=“cabef”
,根據上面給出的計算公式,則他們的 Levenshtein Distance
的計算過程如下:
本文的 demo
使用插件 list-diff2
算法進行比較,該算法的時間復雜度偉 O(n*m)
,雖然該算法并非最優的算法,但是用于對于 dom
元素的常規操作是足夠的。該算法具體的實現過程這里不再詳細介紹,該算法的具體介紹可以參照:github.com/livoras/lis…
(4)實例輸出
兩個虛擬 DOM
對象如下圖所示,其中 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);
我們查看輸出的兩個虛擬 DOM
對象之間的差異對象如下圖所示,我們能通過差異對象得到,兩個虛擬 DOM
對象之間進行了哪些變化,從而根據這個差異對象(patches
)更改原先的真實 DOM
結構,從而將頁面的 DOM
結構進行更改。
DOM
對象的差異應用到真正的 DOM
樹(1)深度優先遍歷 DOM
樹
因為步驟一所構建的 JavaScript
對象樹和 render
出來真正的 DOM
樹的信息、結構是一樣的。所以我們可以對那棵 DOM
樹也進行深度優先的遍歷,遍歷的時候從步驟二生成的 patches
對象中找出當前遍歷的節點差異,如下相關代碼所示:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { // 從patches拿出當前節點的差異 var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 深度遍歷子節點 for (var i = 0; i < len; i++) { var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } // 對當前節點進行DOM操作 if (currentPatches) { applyPatches(node, currentPatches) } }
(2)對原有 DOM
樹進行 DOM
操作
我們根據不同類型的差異對當前節點進行不同的 DOM
操作 ,例如如果進行了節點替換,就進行節點替換 DOM
操作;如果節點文本發生了改變,則進行文本替換的 DOM
操作;以及子節點重排、屬性改變等 DOM
操作,相關代碼如 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結構改變
通過將第 2.2.2 得到的兩個 DOM
對象之間的差異,應用到第一個(原先)DOM
結構中,我們可以看到 DOM
結構進行了預期的變化,如下圖所示:
相關代碼實現已經放到 github 上面,有興趣的同學可以clone運行實驗,github地址為:github.com/fengshi123/…
Virtual DOM
算法主要實現上面三個步驟來實現:
用 JS
對象模擬 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>
比較兩棵虛擬 DOM
樹的差異 — diff.js
將兩個虛擬 DOM
對象的差異應用到真正的 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) } }) }
Vue
源碼 Virtual-DOM
簡析我們從第二章節(Virtual-DOM
基礎)中已經掌握 Virtual DOM
渲染成真實的 DOM
實際上要經歷 VNode
的定義、diff
、patch
等過程,所以本章節 Vue
源碼的解析也按這幾個過程來簡析。
VNode
模擬 DOM
樹VNode
類簡析在 Vue.js
中,Virtual DOM
是用 VNode
這個 Class
去描述,它定義在 src/core/vdom/vnode.js
中 ,從以下代碼塊中可以看到 Vue.js
中的 Virtual DOM
的定義較為復雜一些,因為它這里包含了很多 Vue.js
的特性。實際上 Vue.js
中 Virtual DOM
是借鑒了一個開源庫 snabbdom 的實現,然后加入了一些 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 } }
這里千萬不要因為 VNode
的這么屬性而被嚇到,或者咬緊牙去摸清楚每個屬性的意義,其實,我們主要了解其幾個核心的關鍵屬性就差不多了,例如:
tag
屬性即這個vnode
的標簽屬性
data
屬性包含了最后渲染成真實dom
節點后,節點上的class
,attribute
,style
以及綁定的事件
children
屬性是vnode
的子節點
text
屬性是文本屬性
elm
屬性為這個vnode
對應的真實dom
節點
key
屬性是vnode
的標記,在diff
過程中可以提高diff
的效率
VNode
過程(1)初始化vue
我們在實例化一個 vue
實例,也即 new Vue( )
時,實際上是執行 src/core/instance/index.js
中定義的 Function
函數。
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) }
通過查看 Vue
的 function
,我們知道 Vue
只能通過 new
關鍵字初始化,然后調用 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
實例掛載
Vue
中是通過 $mount
實例方法去掛載 dom
的,下面我們通過分析 compiler
版本的 mount
實現,相關源碼在目錄 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) }
我們發現最終還是調用用原先原型上的 $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) }
我們發現$mount
方法實際上會去調用 mountComponent
方法,這個方法定義在 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) } } // 實例化一個渲染Watcher,在它的回調函數中會調用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
從上面的代碼可以看到,mountComponent
核心就是先實例化一個渲染Watcher
,在它的回調函數中會調用 updateComponent
方法,在此方法中調用 vm._render
方法先生成虛擬 Node,最終調用 vm._update
更新 DOM
。
(3)創建虛擬 Node
Vue
的 _render
方法是實例的一個私有方法,它用來把實例渲染成一個虛擬 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 // 調用 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 }
Vue.js
利用 _createElement
方法創建 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) { // 場景是 render 函數不是編譯生成的 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { // 場景是 render 函數是編譯生成的 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)) { // 創建虛擬 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 個參數,context
表示 VNode 的上下文環境,它是 Component
類型;tag
表示標簽,它可以是一個字符串,也可以是一個 Component
;data
表示 VNode 的數據,它是一個 VNodeData
類型,可以在 flow/vnode.js
中找到它的定義;children
表示當前 VNode 的子節點,它是任意類型的,需要被規范為標準的 VNode
數組;
為了更直觀查看我們平時寫的 Vue
代碼如何用 VNode
類來表示,我們通過一個實例的轉換進行更深刻了解。
例如,實例化一個 Vue
實例:
var app = new Vue({ el: '#app', render: function (createElement) { return createElement('div', { attrs: { id: 'app', class: "class_box" }, }, this.message) }, data: { message: 'Hello Vue!' } })
我們打印出其對應的 VNode
表示:
diff
過程Vue.js
源碼的 diff
調用邏輯Vue.js
源碼實例化了一個 watcher
,這個 ~ 被添加到了在模板當中所綁定變量的依賴當中,一旦 model
中的響應式的數據發生了變化,這些響應式的數據所維護的 dep
數組便會調用 dep.notify()
方法完成所有依賴遍歷執行的工作,這包括視圖的更新,即 updateComponent
方法的調用。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) } } // 實例化一個渲染Watcher,在它的回調函數中會調用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
完成視圖的更新工作事實上就是調用了vm._update
方法,這個方法接收的第一個參數是剛生成的Vnode
,調用的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) { // 第一個參數為真實的node節點,則為初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 如果需要diff的prevVnode存在,那么對prevVnode和vnode進行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 } }
在這個方法當中最為關鍵的就是 vm.__patch__
方法,這也是整個 virtual-dom
當中最為核心的方法,主要完成了prevVnode
和 vnode
的 diff
過程并根據需要操作的 vdom
節點打 patch
,最后生成新的真實 dom
節點并完成視圖的更新工作。
接下來,讓我們看下 vm.__patch__
的邏輯過程, vm.__patch__
方法定義在 src/core/vdom/patch.js
中。
function patch (oldVnode, vnode, hydrating, removeOnly) { ...... if (isUndef(oldVnode)) { // 當oldVnode不存在時,創建新的節點 isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // 對oldVnode和vnode進行diff,并對oldVnode打patch const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } ...... } }
在 patch
方法中,我們看到會分為兩種情況,一種是當 oldVnode
不存在時,會創建新的節點;另一種則是已經存在 oldVnode
,那么會對 oldVnode
和 vnode
進行 diff
及 patch
的過程。其中 patch
過程中會調用 sameVnode
方法來對對傳入的2個 vnode
進行基本屬性的比較,只有當基本屬性相同的情況下才認為這個2個vnode
只是局部發生了更新,然后才會對這2個 vnode
進行 diff
,如果2個 vnode
的基本屬性存在不一致的情況,那么就會直接跳過 diff
的過程,進而依據 vnode
新建一個真實的 dom
,同時刪除老的 dom
節點。
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
過程中主要是通過調用 patchVnode
方法進行的:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { ...... const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children // 如果vnode沒有文本節點 if (isUndef(vnode.text)) { // 如果oldVnode的children屬性存在且vnode的children屬性也存在 if (isDef(oldCh) && isDef(ch)) { // updateChildren,對子節點進行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的內容,然后將vnode的children添加進去 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有子節點,而vnode沒有,那么就清空這個節點 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節點的文本元素 nodeOps.setTextContent(elm, vnode.text) } ...... }
從以上代碼得知,
diff
過程中又分了好幾種情況,oldCh
為 oldVnode
的子節點,ch
為 Vnode
的子節點:
首先進行文本節點的判斷,若 oldVnode.text !== vnode.text
,那么就會直接進行文本節點的替換;
在vnode
沒有文本節點的情況下,進入子節點的 diff
;
當 oldCh
和 ch
都存在且不相同的情況下,調用 updateChildren
對子節點進行 diff
;
若 oldCh
不存在,ch
存在,首先清空 oldVnode
的文本節點,同時調用 addVnodes
方法將 ch
添加到elm
真實 dom
節點當中;
若 oldCh
存在,ch
不存在,則刪除 elm
真實節點下的 oldCh
子節點;
若 oldVnode
有文本節點,而 vnode
沒有,那么就清空這個文本節點。
diff
流程分析(1)Vue.js
源碼
這里著重分析下updateChildren
方法,它也是整個 diff
過程中最重要的環節,以下為 Vue.js
的源碼過程,為了更形象理解 diff
過程,我們給出相關的示意圖來講解。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 為oldCh和newCh分別建立索引,為之后遍歷的依據 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被遍歷完后跳出循環 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) } }
在開始遍歷 diff
前,首先給 oldCh
和 newCh
分別分配一個 startIndex
和 endIndex
來作為遍歷的索引,當oldCh
或者 newCh
遍歷完后(遍歷完的條件就是 oldCh
或者 newCh
的 startIndex >= endIndex
),就停止oldCh
和 newCh
的 diff
過程。接下來通過實例來看下整個 diff
的過程(節點屬性中不帶 key
的情況)。
(2)無 key
的 diff
過程
我們通過以下示意圖對以上代碼過程進行講解:
(2.1)首先從第一個節點開始比較,不管是 oldCh
還是 newCh
的起始或者終止節點都不存在 sameVnode
,同時節點屬性中是不帶 key
標記的,因此第一輪的 diff
完后,newCh
的 startVnode
被添加到 oldStartVnode
的前面,同時 newStartIndex
前移一位;
(2.2)第二輪的 diff
中,滿足 sameVnode(oldStartVnode, newStartVnode)
,因此對這2個 vnode
進行diff
,最后將 patch
打到 oldStartVnode
上,同時 oldStartVnode
和 newStartIndex
都向前移動一位 ;
(2.3)第三輪的 diff
中,滿足 sameVnode(oldEndVnode, newStartVnode)
,那么首先對 oldEndVnode
和newStartVnode
進行 diff
,并對 oldEndVnode
進行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;
(2.4)第四輪的 diff
中,過程同步驟3;
(2.5)第五輪的 diff
中,同過程1;
(2.6)遍歷的過程結束后,newStartIdx > newEndIdx
,說明此時 oldCh
存在多余的節點,那么最后就需要將這些多余的節點刪除。
(3)有 key
的 diff
流程
在 vnode
不帶 key
的情況下,每一輪的 diff
過程當中都是起始
和結束
節點進行比較,直到 oldCh
或者newCh
被遍歷完。而當為 vnode
引入 key
屬性后,在每一輪的 diff
過程中,當起始
和結束
節點都沒有找到sameVnode
時,然后再判斷在 newStartVnode
的屬性中是否有 key
,且是否在 oldKeyToIndx
中找到對應的節點 :
如果不存在這個 key
,那么就將這個 newStartVnode
作為新的節點創建且插入到原有的 root
的子節點中;
如果存在這個 key
,那么就取出 oldCh
中的存在這個 key
的 vnode
,然后再進行 diff
的過;
通過以上分析,給vdom
上添加 key
屬性后,遍歷 diff
的過程中,當起始點,結束點的搜尋及 diff
出現還是無法匹配的情況下時,就會用 key
來作為唯一標識,來進行 diff
,這樣就可以提高 diff
效率。
帶有 Key
屬性的 vnode
的 diff
過程可見下圖:
(3.1)首先從第一個節點開始比較,不管是 oldCh
還是 newCh
的起始或者終止節點都不存在 sameVnode
,但節點屬性中是帶 key
標記的, 然后在 oldKeyToIndx
中找到對應的節點,這樣第一輪 diff
過后 oldCh
上的B節點
被刪除了,但是 newCh
上的B節點
上 elm
屬性保持對 oldCh
上 B節點
的elm
引用。
(3.2)第二輪的 diff
中,滿足 sameVnode(oldStartVnode, newStartVnode)
,因此對這2個 vnode
進行diff
,最后將 patch
打到 oldStartVnode
上,同時 oldStartVnode
和 newStartIndex
都向前移動一位 ;
(3.3)第三輪的 diff
中,滿足 sameVnode(oldEndVnode, newStartVnode)
,那么首先對 oldEndVnode
和newStartVnode
進行 diff
,并對 oldEndVnode
進行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;
(3.4)第四輪的diff
中,過程同步驟2;
(3.5)第五輪的diff
中,因為此時 oldStartIndex
已經大于 oldEndIndex
,所以將剩余的 Vnode
隊列插入隊列最后。
patch
過程通過3.2章節介紹的 diff
過程中,我們會看到 nodeOps
相關的方法對真實 DOM
結構進行操作,nodeOps
定義在 src/platforms/web/runtime/node-ops.js
中,其為基本 DOM
操作,這里就不在詳細介紹。
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) }
關于“Vue中的虛擬DOM如何構建”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Vue中的虛擬DOM如何構建”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。