中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

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

發布時間:2020-07-13 15:51:10 來源:網絡 閱讀:266 作者:wx5d61fdc401976 欄目:開發技術

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

辛苦編寫良久,還望手動點贊鼓勵~

github地址為:github.com/fengshi123/…,上面匯總了作者所有的博客文章,如果喜歡或者有所啟發,請幫忙給個 star ~,對作者也是一種鼓勵。

一、真實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 基礎
2.1、虛擬 DOM 的好處
? 虛擬 DOM 就是為了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有 10 次更新 DOM 的動作,虛擬 DOM 不會立即操作 DOM,而是將這 10 次更新的 diff 內容保存到本地一個 JS 對象中,最終將這個 JS 對象一次性 attch 到 DOM 樹上,再進行后續操作,避免大量無謂的計算量。所以,用 JS 對象模擬 DOM 節點的好處是,頁面的更新可以先全部反映在 JS 對象(虛擬 DOM )上,操作內存中的 JS 對象的速度顯然要更快,等更新完成后,再將最終的 JS 對象映射成真實的 DOM,交由瀏覽器去繪制。

2.2、算法實現
2.2.1、用 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 結構,效果如下圖所示:

2.2.2、比較兩棵虛擬 DOM 樹的差異 — diff 算法
diff 算法用來比較兩棵 Virtual DOM 樹的差異,如果需要兩棵樹的完全比較,那么 diff 算法的時間復雜度為O(n^3)。但是在前端當中,你很少會跨越層級地移動 DOM 元素,所以 Virtual DOM 只會對同一個層級的元素進行對比,如下圖所示, div 只會和同一層級的 div 對比,第二層級的只會跟第二層級對比,這樣算法復雜度就可以達到 O(n)。

(1)深度優先遍歷,記錄差異

在實際的代碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每個節點都會有一個唯一的標記:

dfs-walk
在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的的樹進行對比。如果有差異的話就記錄到一個對象里面。

// 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 結構進行更改。

2.2.3、將兩個虛擬 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 結構進行了預期的變化,如下圖所示:

2.3、結語
相關代碼實現已經放到 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 源碼的解析也按這幾個過程來簡析。

3.1、VNode 模擬 DOM 樹
3.1.1、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;
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
}
}
復制代碼
這里千萬不要因為 VNode 的這么屬性而被嚇到,或者咬緊牙去摸清楚每個屬性的意義,其實,我們主要了解其幾個核心的關鍵屬性就差不多了,例如:

tag 屬性即這個vnode的標簽屬性
data 屬性包含了最后渲染成真實dom節點后,節點上的class,attribute,style以及綁定的事件
children 屬性是vnode的子節點
text 屬性是文本屬性
elm 屬性為這個vnode對應的真實dom節點
key 屬性是vnode的標記,在diff過程中可以提高diff的效率
3.1.2、源碼創建 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 數組;

3.1.3、實例查看
為了更直觀查看我們平時寫的 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 表示:

3.2、diff 過程
3.2.1、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 沒有,那么就清空這個文本節點。
3.2.2、子節點 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 隊列插入隊列最后。

?????????è?°
3.3、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)
}
復制代碼
3.4、總結
通過前三小節簡析,我們從主線上把模板和數據如何渲染成最終的 DOM 的過程分析完畢了,我們可以通過下圖更直觀地看到從初始化 Vue 到最終渲染的整個過程。

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

辛苦編寫良久,如果對你有幫助,還望手動點贊鼓勵~~

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

九江市| 额尔古纳市| 永泰县| 临夏市| 牡丹江市| 太康县| 东阿县| 汉川市| 海门市| 辽中县| 霍州市| 闻喜县| 临泽县| 仪征市| 尖扎县| 吉安市| 乌海市| 禹城市| 理塘县| 安远县| 白朗县| 科技| 辽阳市| 平江县| 双流县| 阿克陶县| 南康市| 江都市| 阿拉善左旗| 陆良县| 古田县| 泽州县| 方山县| 阳春市| 启东市| 黔东| 安龙县| 宜城市| 文安县| 中江县| 翁牛特旗|