使用 Vue
作項目也有兩年時間了,對 Vue
的 api
也用的比較駕輕就熟了,雖然對 Vue
的一些實現原理也耳有所聞,例如 虛擬DOM
、flow
、數據驅動、路由原理等等,可是本身並無特地去探究這些原理的基礎以及 Vue
源碼是如何利用這些原理進行框架實現的,因此利用空閒時間,進行 Vue
框架相關技術原理和 Vue
框架的具體實現的整理。若是你對 Vue
的實現原理很感興趣,那麼就能夠開始這系列文章的閱讀,將會爲你打開 Vue
的底層世界大門,對它的實現細節一探究竟。 本文爲 Virtual DOM
的技術原理和 Vue
框架的具體實現。javascript
辛苦編寫良久,還望手動點贊鼓勵~前端
github地址爲:github.com/fengshi123/…,上面彙總了做者全部的博客文章,若是喜歡或者有所啓發,請幫忙給個 star ~,對做者也是一種鼓勵。vue
DOM
和其解析流程 本節咱們主要介紹真實 DOM
的解析過程,經過介紹其解析過程以及存在的問題,從而引出爲何須要虛擬DOM
。一圖勝千言,以下圖爲 webkit
渲染引擎工做流程圖java
全部的瀏覽器渲染引擎工做流程大體分爲5步:建立 DOM
樹 —> 建立 Style Rules
-> 構建 Render
樹 —> 佈局 Layout
-—> 繪製 Painting
。node
注意點:git
一、DOM
樹的構建是文檔加載完成開始的? 構建 DOM
樹是一個漸進過程,爲達到更好的用戶體驗,渲染引擎會盡快將內容顯示在屏幕上,它沒必要等到整個 HTML
文檔解析完成以後纔開始構建 render
樹和佈局。github
二、Render
樹是 DOM
樹和 CSS
樣式表構建完畢後纔開始構建的? 這三個過程在實際進行的時候並非徹底獨立的,而是會有交叉,會一邊加載,一邊解析,以及一邊渲染。web
三、CSS
的解析注意點? CSS
的解析是從右往左逆向解析的,嵌套標籤越多,解析越慢。算法
四、JS
操做真實 DOM
的代價? 用咱們傳統的開發模式,原生 JS
或 JQ
操做 DOM
時,瀏覽器會從構建 DOM 樹開始從頭至尾執行一遍流程。在一次操做中,我須要更新 10 個 DOM
節點,瀏覽器收到第一個 DOM
請求後並不知道還有 9 次更新操做,所以會立刻執行流程,最終執行10 次。例如,第一次計算完,緊接着下一個 DOM
更新請求,這個節點的座標值就變了,前一次計算爲無用功。計算 DOM
節點座標值等都是白白浪費的性能。即便計算機硬件一直在迭代更新,操做 DOM
的代價仍舊是昂貴的,頻繁操做仍是會出現頁面卡頓,影響用戶體驗segmentfault
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
換成 h1
;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)
}
複製代碼
經過前三小節簡析,咱們從主線上把模板和數據如何渲染成最終的 DOM
的過程分析完畢了,咱們能夠經過下圖更直觀地看到從初始化 Vue
到最終渲染的整個過程。
本文從經過介紹真實 DOM
結構其解析過程以及存在的問題,從而引出爲何須要虛擬 DOM
;而後分析虛擬DOM
的好處,以及其一些理論基礎和基礎算法的實現;最後根據咱們已經掌握的基礎知識,再一步步去查看Vue.js
的源碼如何實現的。從存在問題 —> 理論基礎 —> 具體實踐,一步步深刻,幫助你們更好的瞭解什麼是Virtual DOM
、爲何須要 Virtual DOM
、以及 Virtual DOM
的具體實現,但願本文對您有幫助。
**辛苦編寫良久,若是對你有幫助,還望手動點贊鼓勵~~~~~~**
github地址爲:github.com/fengshi123/…,上面彙總了做者全部的博客文章,若是喜歡或者有所啓發,請幫忙給個 star ~,對做者也是一種鼓勵。
一、Vue 技術揭祕:ustbhuangyi.github.io/vue-analysi…
二、深度剖析:如何實現一個 Virtual DOM 算法:segmentfault.com/a/119000000…
三、vue核心之虛擬DOM(vdom):www.jianshu.com/p/af0b39860…
四、virtual-dom(Vue實現)簡析:segmentfault.com/a/119000001…