在這篇文章深刻源碼學習Vue響應式原理講解了當數據更改時,Vue
是如何通知依賴他的數據進行更新的,這篇文章講得就是:視圖知道了依賴的數據的更改,可是怎麼更新視圖的。html
在真實的HTML
中有DOM
樹與之對應,在Vue
中也有相似的Vnode Tree
與之對應。vue
DOM
樹在jquery
時代,實現一個功能,每每是直接對DOM
進行操做來達到改變視圖的目的。可是咱們知道直接操做DOM
每每會影響重繪和重排,這兩個是最影響性能的兩個元素。
進入Virtual DOM
時代之後,將真實的DOM
樹抽象成了由js
對象構成的抽象樹。virtual DOM
就是對真實DOM
的抽象,用屬性來描述真實DOM
的各類特性。當virtual DOM
發生改變時,就去修改視圖。在Vue
中就是Vnode Tree
的概念node
當修改某條數據的時候,這時候js
會將整個DOM Tree
進行替換,這種操做是至關消耗性能的。因此在Vue
中引入了Vnode
的概念:Vnode
是對真實DOM
節點的模擬,能夠對Vnode Tree
進行增長節點、刪除節點和修改節點操做。這些過程都只須要操做VNode Tree
,不須要操做真實的DOM
,大大的提高了性能。修改以後使用diff
算法計算出修改的最小單位,在將這些小單位的視圖進行更新。jquery
// core/vdom/vnode.js
class Vnode {
constructor(tag, data, children, text, elm, context, componentOptions) {
// ...
}
}
複製代碼
vnode
生成vnode
有兩種狀況:git
vnode
tag
不存在,建立空節點、註釋、文本節點vue
內部列出的元素類型的vnode
vnode
以<p>123</p>
爲例,會被生成兩個vnode
:github
tag
爲p
,可是沒有text
值的節點tag
類型,可是有text
值的節點VNode
組件節點生成的Vnode
,不會和DOM Tree
的節點一一對應,只存在VNode Tree
中// core/vdom/create-component
function createComponent() {
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }
)
}
複製代碼
這裏建立一個組件佔位vnode
,也就不會有真實的DOM
節點與之對應組件vnode
的創建,結合下面例子進行講解:算法
<!--parent.vue-->
<div classs="parent">
<child></child>
</div>
<!--child.vue-->
<template>
<div class="child"></div>
</template>
複製代碼
真實渲染出來的DOM Tree
是不會存在child
這個標籤的。child.vue
是一個子組件,在Vue
中會給這個組件建立一個佔位的vnode
,這個vnode
在最終的DOM Tree
不會與DOM
節點一一對應,即只會出現vnode Tree
中。數組
/* core/vdom/create-component.js */
export function createComponent () {
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }
)
}
複製代碼
那最後生成的Vnode Tree
就大概以下:bash
vue-component-${cid}-parent
vue-component-${cid}-child
div.child
複製代碼
最後生成的DOM
結構爲:app
<div class="parent">
<div class="child"></div>
</div>
複製代碼
在兩個組件文件中打印自身,能夠看出二者之間的關係 chlid
實例對象
parent
實例對象
能夠看到如下關係:
vnode
經過children
指向子vnode
vnode
經過$parent
指向父vnode
vnode
爲對象的$vnode
vnode
爲對象的_vnode
patch
在上一篇文章提到當建立Vue
實例的時候,會執行如下代碼:
updateComponent = () => {
const vnode = vm._render();
vm._update(vnode)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
複製代碼
當數據發生改變時會觸發回調函數updateComponent
進行模板數據更新,updateComponent
其實是對__patch__
的封裝。patch
的本質是將新舊vnode
進行比較,建立或者更新DOM
節點/組件實例,若是是首次的話,那麼就建立DOM
或者組件實例。
// core/vdom/patch.js
function createPatchFunction(backend) {
const { modules, nodeOps } = backend;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
return function patch(oldVnode, vnode) {
if (isUndef(oldVnode)) {
let isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveC ? null : parentELm,
nodeOps.nextSibling(oldElm)
)
if (isDef(vnode.parent)) {
let ancestor = vnode.parent;
while(ancestor) {
ancestor.elm = vnode.elm;
ancestor = ancestor.parent
}
if (isPatchable(vnode)) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent)
}
}
}
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue)
return vode.elm
}
}
複製代碼
patch
,就建立一個新的節點DOM
而且和新節點相同
patchVnode
修改現有節點DOM
,建立真實的DOM
Vnode
建立元素/組件實例,若parentElm
存在,則插入到父元素上element
。而後移除老節點insert
鉤子
patch
而且vnode.parent
存在,設置vnode.parent.data.pendingInsert = queue
vnode
調用insert
鉤子vnode.elm
nodeOps
上封裝了針對各類平臺對於DOM
的操做,modules
表示各類模塊,這些模塊都提供了create
和update
鉤子,用於建立完成和更新完成後處理對應的模塊;有些模塊還提供了activate
、remove
、destory
等鉤子。通過處理後cbs
的最終結構爲:cbs = {
create: [
attrs.create,
events.create
// ...
]
}
複製代碼
最後該函數返回的就是patch
方法。
createElm
createElm
的目的建立VNode
節點的vnode.elm
。不一樣類型的VNode
,其vnode.elm
建立過程也不同。對於組件佔位VNode
,會調用createComponent
來建立組件佔位VNode
的組件實例;對於非組件佔位VNode
會建立對應的DOM
節點。 如今有三種節點:
VNode
:
vnode
對應的DOM
元素節點vnode.elm
vnode
的scope
createChildren
建立子vnode
的DOM
節點create
鉤子函數DOM
元素插入到父元素中vnode.elm
,並插入到父元素中createComponent
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
// 建立一個組件節點
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data;
const childre = vnode.children;
const tag = vnode.tag;
// ...
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
createChildren(vnode, children, insertedVnodeQueue)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
} else {
vnode.elm = nodeOps.createTextNode(vnode.te)
}
insert(parentElm, vnode.elm, refElm)
}
複製代碼
createComponent
的主要做用是在於建立組件佔位Vnode
的組件實例, 初始化組件,而且從新激活組件。在從新激活組件中使用insert
方法操做DOM
。createChildren
用於建立子節點,若是子節點是數組,則遍歷執行createElm
方法,若是子節點的text
屬性有數據,則使用nodeOps.appendChild()
在真實DOM
中插入文本內容。insert
用將元素插入到真實DOM
中。
// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
// ...
let i = vnode.data.hook.init
i(vnode, false, parentElm, refElm)
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
return true;
}
}
複製代碼
init
鉤子生成componentInstance
組件實例initComponent
初始化組件
vnode
隊列進行合併DOM
根元素節點,賦給vnode.elm
vnode
是可patch
create
函數,設置scope
patch
ref
,把組件佔位vnode
加入insertedVnodeQueue
vnode.elm
插入到DOM Tree
中在組件建立過程當中會調用core/vdom/create-component
中的createComponent
,這個函數會建立一個組件VNode
,而後會再vnode
上建立聲明各個聲明周期函數,init
就是其中的一個週期,他會爲vnode
建立componentInstance
屬性,這裏componentInstance
表示繼承Vue
的一個實例。在進行new vnodeComponentOptions.Ctor(options)
的時候就會從新建立一個vue
實例,也就會從新把各個生命週期執行一遍如created-->mounted
。
init (vnode) {
// 建立子組件實例
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
chid.$mount(undefined)
}
function createComponentInstanceForVnode(vn) {
// ... options的定義
return new vnodeComponentOptions.Ctor(options)
}
複製代碼
這樣child
就表示一個Vue
實例,在實例建立的過程當中,會執行各類初始化操做, 例如調用各個生命週期。而後調用$mount
,實際上會調用mountComponent
函數,
// core/instance/lifecycle
function mountComponent(vm, el) {
// ...
updateComponent = () => {
vm._update(vm._render())
}
vm._watcher = new Watcher(vm, updateComponent, noop)
}
複製代碼
在這裏就會執行vm._render
// core/instance/render.js
Vue.propotype._render = function () {
// ...
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
複製代碼
能夠看到的時候調用_render
函數,最後生成了一個vnode
。而後調用vm._update
進而調用vm.__patch__
生成組件的DOM Tree
,可是不會把DOM Tree
插入到父元素上,若是子組件中還有子組件,就會建立子孫組件的實例,建立子孫組件的DOM Tree
。當調用insert(parentElm, vnode.elm, refElm)
纔會將當前的DOM Tree
插入到父元素中。
在回到patch
函數,當不是第一次渲染的時候,就會執行到另外的邏輯,而後oldVnode
是否爲真實的DOM
,若是不是,而且新老VNode
不相同,就執行patchVnode
。
// core/vdom/patch.js
function sameVnode(a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType
)
}
複製代碼
sameVnode
就是用於判斷兩個vnode
是不是同一個節點。
patchVnode
若是符合sameVnode
,就不會渲染vnode
從新建立DOM
節點,而是在原有的DOM
節點上進行修補,儘量複用原有的DOM
節點。
vnode
是可patch
的
vnode
的prepatch
鉤子update
鉤子存在,調用update
鉤子vnode
不存在text
文本
children
子節點,且children
不相同,則調用updateChildren
遞歸更新children
(這個函數的內容放到diff
中進行講解)vnode
的postpatch
鉤子function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) return
// 靜態節點的處理程序
const data = vnode.data;
i = data.hook.prepatch
i(oldVnode, vnode);
if (isPatchable(vnode)) {
for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
i = data.hook.update
i(oldVnode, vnode)
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
i = data.hook.postpatch
i(oldVnode, vnode)
}
複製代碼
diff
算法在patchVnode
中提到,若是新老節點都有子節點,可是不相同的時候就會調用updateChildren
,這個函數經過diff
算法儘量的複用先前的DOM
節點。
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
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, elmToMove, refElm
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
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] : null
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
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)
}
}
複製代碼
算了這個圖沒畫明白,借用網上的圖
oldStartIdx
、
newStartIdx
、
oldEndIdx
以及
newEndIdx
分別是新老兩個
VNode
兩邊的索引,同時
oldStartVnode
、
newStartVnode
、
oldEndVnode
和
new EndVnode
分別指向這幾個索引對應的
vnode
。整個遍歷須要在
oldStartIdx
小於
oldEndIdx
而且
newStartIdx
小於
newEndIdx
(這裏爲了簡便,稱
sameVnode
爲類似)
oldStartVnode
不存在的時候,oldStartVnode
向右移動,oldStartIdx
加1
oldEndVnode
不存在的時候,oldEndVnode
向右移動,oldEndIdx
減1
oldStartVnode
和newStartVnode
類似,oldStartVnode
和newStartVnode
都向右移動,oldStartIdx
和newStartIdx
都增長1
oldEndVnode
和newEndVnode
類似,oldEndVnode
和newEndVnode
都向左移動,oldEndIdx
和newEndIdx
都減1
oldStartVnode
和newEndVnode
類似,則把oldStartVnode.elm
移動到oldEndVnode.elm
的節點後面。而後oldStartIdx
向後移動一位,newEndIdx
向前移動一位oldEndVnode
和
newStartVnode
類似時,把
oldEndVnode.elm
插入到
oldStartVnode.elm
前面。一樣的,
oldEndIdx
向前移動一位,
newStartIdx
向後移動一位。
7. 當以上狀況都不符合的時候 生成一個
key
與舊
vnode
對應的哈希表
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
複製代碼
最後生成的對象就是以children
的key
爲屬性,遞增的數字爲屬性值的對象例如
children = [{
key: 'key1'
}, {
key: 'key2'
}]
// 最後生成的map
map = {
key1: 0,
key2: 1,
}
複製代碼
因此oldKeyToIdx
就是key
和舊vnode
的key
對應的哈希表 根據newStartVnode
的key
看可否找到對應的oldVnode
oldVnode
不存在,就建立一個新節點,newStartVnode
向右移動newStartVnode
類似。將map
表中該位置的賦值undefined
(用於保證key
是惟一的)。同時將newStartVnode.elm
插入啊到oldStartVnode.elm
的前面,而後index
向後移動一位sameVnode
,只能建立一個新節點插入到parentElm
的子節點中,newStartIdx
向後移動一位結束循環後
oldStartIdx
又大於oldEndIdx
,就將新節點中沒有對比的節點加到隊尾中newStartIdx > newEndIdx
,就說明還存在新節點,就將這些節點進行刪除本篇文章對數據發生改變時,視圖是如何更新進行了講解。對一些細節地方進行了省略,若是須要了解更加深刻,結合源碼更加合適。個人github請多多關注,謝謝