Vitual DOM
是一種虛擬dom
技術,本質上是基於javascript
實現的,相對於dom
對象,javascript
對象更簡單,處理速度更快,dom
樹的結構,屬性信息均可以很容易的用javascript
對象來表示:javascript
let element={ tagName:'ul',//節點標籤名 props:{//dom的屬性,用一個對象存儲鍵值對 id:'list' }, children:[//該節點的子節點 {tagName:'li',props:{class:'item'},children:['aa']}, {tagName:'li',props:{class:'item'},children:['bb']}, {tagName:'li',props:{class:'item'},children:['cc']} ] } 對應的html寫法是: <ul id='list'> <li class='item'>aa</li> <li class='item'>aa</li> <li class='item'>aa</li> </ul>
Virtual DOM
並無徹底實現DOM
,Virtual DOM
最主要的仍是保留了Element
之間的層次關係和一些基本屬性. 你給我一個數據,我根據這個數據生成一個全新的Virtual DOM
,而後跟我上一次生成的Virtual DOM
去 diff
,獲得一個Patch
,而後把這個Patch
打到瀏覽器的DOM
上去。html
咱們能夠經過javascript
對象表示的樹結構來構建一棵真正的dom
樹,當數據狀態發生變化時,能夠直接修改這個javascript
對象,接着對比修改後的javascript
對象,記錄下須要對頁面作的dom
操做,而後將其應用到真正的dom
樹,實現視圖的更新,這個過程就是Virtual DOM
的核心思想。vue
VNode
的數據結構圖:
java
VNode
生成最關鍵的點是經過render
有2
種生成方式,第一種是直接在vue
對象的option
中添加render
字段。第二種是寫一個模板或指定一個el
根元素,它會首先轉換成模板,通過html
語法解析器生成一個ast
抽象語法樹,對語法樹作優化,而後把語法樹轉換成代碼片斷,最後經過代碼片斷生成function
添加到option
的render
字段中。node
ast
語法優的過程,主要作了2
件事:segmentfault
class
名和attributes
,這樣它們在初始化渲染後就永遠不會再被比對了。vnode
,同時跳過比對。src/core/vdom/create-element.js const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不傳data的狀況 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 若是alwaysNormalize是true // 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 調用_createElement建立虛擬節點 return _createElement(context, tag, data, children, normalizationType) } function _createElement (context, tag, data, children, normalizationType) { /** * 若是存在data.__ob__,說明data是被Observer觀察的數據 * 不能用做虛擬節點的data * 須要拋出警告,並返回一個空節點 * 被監控的data不能被用做vnode渲染的數據的緣由是: * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做 */ if (data && data.__ob__) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // 當組件的is屬性被設置爲一個falsy的值 // Vue將不會知道要把這個組件渲染成什麼 // 因此渲染一個空節點 if (!tag) { return createEmptyVNode() } // 做用域插槽 if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根據normalizationType的值,選擇不一樣的處理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 若是標籤名是字符串類型 if (typeof tag === 'string') { let Ctor // 獲取標籤名的命名空間 ns = config.getTagNamespace(tag) // 判斷是否爲保留標籤 if (config.isReservedTag(tag)) { // 若是是保留標籤,就建立一個這樣的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義 } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) { // 若是找到了這個標籤的定義,就以此建立虛擬組件節點 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常建立一個vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 當tag不是字符串的時候,咱們認爲tag是組件的構造類 // 因此直接建立 } else { vnode = createComponent(tag, data, context, children) } // 若是有vnode if (vnode) { // 若是有namespace,就應用下namespace,而後返回vnode if (ns) applyNS(vnode, ns) return vnode // 不然,返回一個空節點 } else { return createEmptyVNode() } }
方法的功能是給一個Vnode
對象對象添加若干個子Vnode
,由於整個Virtual DOM
是一種樹狀結構,每一個節點均可能會有若干子節點。而後建立一個VNode
對象,若是是一個reserved tag
(好比html,head
等一些合法的html
標籤)則會建立普通的DOM VNode
,若是是一個component tag
(經過vue
註冊的自定義component
),則會建立Component VNode
對象,它的VnodeComponentOptions
不爲Null
.
建立好Vnode
,下一步就是要把Virtual DOM
渲染成真正的DOM
,是經過patch
來實現的,源碼以下:api
src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // oldVnoe:dom||當前vnode,vnode:vnoder=對象類型,hydration是否直接用服務端渲染的dom元素 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 空掛載(多是組件),建立新的根元素。 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch 現有的根節點 patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // 安裝到一個真實的元素。 // 檢查這是不是服務器渲染的內容,若是咱們能夠執行。 // 成功的水合做用。 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // 不是服務器呈現,就是水化失敗。建立一個空節點並替換它。 oldVnode = emptyNodeAt(oldVnode) } // 替換現有的元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // 極爲罕見的邊緣狀況:若是舊元素在a中,則不要插入。 // 離開過渡。只有結合過渡+時纔會發生。 // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 遞歸地更新父佔位符節點元素。 if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // 調用插入鉤子,這些鉤子可能已經被建立鉤子合併了。 // 例如使用「插入」鉤子的指令。 const insert = ancestor.data.hook.insert if (insert.merged) { // 從索引1開始,以免從新調用組件掛起的鉤子。 for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
patch
支持的3
個參數,其中oldVnode
是一個真實的DOM
或者一個VNode
對象,它表示當前的VNode
,vnode
是VNode
對象類型,它表示待替換的VNode
,hydration
是bool
類型,它表示是否直接使用服務器端渲染的DOM
元素,下面流程圖表示patch
的運行邏輯:瀏覽器
patch
運行邏輯看上去比較複雜,有2
個方法createElm
和patchVnode
是生成dom
的關鍵,源碼以下:服務器
/** * @param vnode根據vnode的數據結構建立真實的dom節點,若是vnode有children則會遍歷這些子節點,遞歸調用createElm方法, * @param insertedVnodeQueue記錄子節點建立順序的隊列,每建立一個dom元素就會往隊列中插入當前的vnode,當整個vnode對象所有轉換成爲真實的dom 樹時,會依次調用這個隊列中vnode hook的insert方法 */ let inPre = 0 function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested // 過渡進入檢查 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { if (data && data.pre) { inPre++ } if ( !inPre && !vnode.ns && !( config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(tag) : ignore === tag }) ) && config.isUnknownElement(tag) ) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) /* istanbul ignore if */ if (__WEEX__) { // in Weex, the default insertion order is parent-first. // List items can be optimized to use children-first insertion // with append="tree". const appendAsTree = isDef(data) && isTrue(data.appendAsTree) if (!appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } createChildren(vnode, children, insertedVnodeQueue) if (appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } if (process.env.NODE_ENV !== 'production' && data && data.pre) { inPre-- } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
方法會根據vnode
的數據結構建立真實的DOM
節點,若是vnode
有children
,則會遍歷這些子節點,遞歸調用createElm
方法,InsertedVnodeQueue
是記錄子節點建立順序的隊列,每建立一個DOM
元素就會往這個隊列中插入當前的VNode
,當整個VNode
對象所有轉換成爲真實的DOM
樹時,會依次調用這個隊列中的VNode hook
的insert
方法。數據結構
/** * 比較新舊vnode節點,根據不一樣的狀態對dom作合理的更新操做(添加,移動,刪除)整個過程還會依次調用prepatch,update,postpatch等鉤子函數,在編譯階段生成的一些靜態子樹,在這個過程 * @param oldVnode 中因爲不會改變而直接跳過比對,動態子樹在比較過程當中比較核心的部分就是當新舊vnode同時存在children,經過updateChildren方法對子節點作更新, * @param vnode * @param insertedVnodeQueue * @param removeOnly */ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 用於靜態樹的重用元素。 // 注意,若是vnode是克隆的,咱們只作這個。 // 若是新節點不是克隆的,則表示呈現函數。 // 由熱重加載api從新設置,咱們須要進行適當的從新渲染。 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } 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) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
updateChildren
方法解析在此:vue:虛擬DOM的patch