1.什么是虛擬DOM

2.引入虛擬DOM的目的
VNode的定義 Vue中定義了VNode的構造函數,這樣我們可以實例化不同的vnode 實例如:文本節點、元素節點以及注釋節點等。
var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { 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其實就是一個描述節點的對象,描述如何創建真實的DOM節點;vnode的作用就是新舊vnode進行對比,只更新發生變化的節點。 VNode有注釋節點、文本節點、元素節點、組件節點、函數式組件、克隆節點:
注釋節點
var createEmptyVNode = function (text) { if ( text === void 0 ) text = ''; var node = new VNode(); node.text = text; node.isComment = true; return node };只有isComment和text屬性有效,其余的默認為false或者null
文本節點
function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val)) }只有一個text屬性
克隆節點
function cloneVNode (vnode) { var cloned = new VNode(  vnode.tag,  vnode.data,  // #7975  // clone children array to avoid mutating original in case of cloning  // a child.  vnode.children && vnode.children.slice(),  vnode.text,  vnode.elm,  vnode.context,  vnode.componentOptions,  vnode.asyncFactory ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.asyncMeta = vnode.asyncMeta; cloned.isCloned = true; return cloned }克隆節點將vnode的所有屬性賦值到clone節點,并且設置isCloned = true,它的作用是優化靜態節點和插槽節點。以靜態節點為例,因為靜態節點的內容是不會改變的,當它首次生成虛擬DOM節點后,再次更新時是不需要再次生成vnode,而是將原vnode克隆一份進行渲染,這樣在一定程度上提升了性能。
元素節點 元素節點一般會存在tag、data、children、context四種有效屬性,形如:
{ children: [VNode, VNode], context: {...}, tag: 'div', data: {attr: {id: app}}}組件節點 組件節點有兩個特有屬性 (1) componentOptions,組件節點的選項參數,包含如下內容:
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }(2) componentInstance: 組件的實例,也是Vue的實例 對應的vnode
new VNode(  ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),  data, undefined, undefined, undefined, context,  { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },  asyncFactory )即
{ componentOptions: {}, componentInstance: {}, tag: 'vue-component-1-child', data: {...}, ...}函數式組件 函數組件通過createFunctionalComponent函數創建, 跟組件節點類似,暫時沒看到特殊屬性,有的話后續再補上。
patch
虛擬DOM最重要的功能是patch,將VNode渲染為真實的DOM。
patch簡介
patch中文意思是打補丁,也就是在原有的基礎上修改DOM節點,也可以說是渲染視圖。DOM節點的修改有三種:
當緩存上一次的oldvnode與最新的vnode不一致的時候,渲染視圖以vnode為準。
初次渲染過程
當oldvnode中不存在,而vnode中存在時,就需要使用vnode新生成真實的DOM節點并插入到視圖中。首先如果vnode具有tag屬性,則認為它是元素屬性,再根據當前環境創建真實的元素節點,元素創建后將它插入到指定的父節點。以上節生成的VNode為例,首次執行
vm._update(vm._render(), hydrating);
vm._render()為上篇生成的VNode,_update函數具體為
Vue.prototype._update = function (vnode, hydrating) {  var vm = this;  var prevEl = vm.$el;  var prevVnode = vm._vnode;  var restoreActiveInstance = setActiveInstance(vm);  // 緩存vnode  vm._vnode = vnode;  // Vue.prototype.__patch__ is injected in entry points  // based on the rendering backend used.  // 第一次渲染,preVnode是不存在的  if (!prevVnode) {  // initial render  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);  } else {  // updates  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;  }  // updated hook is called by the scheduler to ensure that children are  // updated in a parent's updated hook. };因第一次渲染,執行 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); ,注意第一個參數是oldVnode為 vm.$el 為元素節點,__patch__函數具體過程為:
(1) 先判斷oldVnode是否存在,不存在就創建vnode
if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true; createElm(vnode, insertedVnodeQueue);}(2) 存在進入else,判斷oldVnode是否是元素節點,如果oldVnode是元素節點,則
if (isRealElement) { ... // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode);}創建一個oldVnode節點,其形式為
{ asyncFactory: undefined, asyncMeta: undefined, children: [], componentInstance: undefined, componentOptions: undefined, context: undefined, data: {}, elm: div#app, fnContext: undefined, fnOptions: undefined, fnScopeId: undefined, isAsyncPlaceholder: false, isCloned: false, isComment: false, isOnce: false, isRootInsert: true, isStatic: false, key: undefined, ns: undefined, parent: undefined, raw: false, tag: "div", text: undefined, child: undefined}然后獲取oldVnode的元素節點以及其父節點,并創建新的節點
// replacing existing elementvar oldElm = oldVnode.elm;var parentElm = nodeOps.parentNode(oldElm);// create new nodecreateElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
創建新節點的過程
// 標記是否是根節點 vnode.isRootInsert = !nested; // for transition enter check // 這個函數如果vnode有componentInstance屬性,會創建子組件,后續具體介紹,否則不做處理if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return}接著在對子節點處理
var data = vnode.data; var children = vnode.children; var tag = vnode.tag; if (isDef(tag)) { ... vnode.elm = vnode.ns  ? nodeOps.createElementNS(vnode.ns, tag)  : nodeOps.createElement(tag, vnode); setScope(vnode); /* istanbul ignore if */ {  createChildren(vnode, children, insertedVnodeQueue);  if (isDef(data)) {   invokeCreateHooks(vnode, insertedVnodeQueue);  }  insert(parentElm, vnode.elm, refElm); } if (data && data.pre) {  creatingElmInVPre--; } }}將vnode的屬性設置為創建元素節點elem,創建子節點 createChildren(vnode, children, insertedVnodeQueue); 該函數遍歷子節點children數組
function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) {  for (var i = 0; i < children.length; ++i) {   createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);  } } else if (isPrimitive(vnode.text)) {  // 如果vnode是文本直接掛載  nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))); }}遍歷children,遞歸createElm方法創建子元素節點
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);}如果是評論節點,直接創建評論節點,并將其插入到父節點上,其他的創建文本節點,并將其插入到父節點parentElm(剛創建的div)上去。 觸發鉤子,更新節點屬性,將其插入到parentElm('#app'元素節點)上
{ createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) {  invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm);}最后將老的節點刪掉
if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0);} else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode);}function removeAndInvokeRemoveHook (vnode, rm) { if (isDef(rm) || isDef(vnode.data)) {  var i;  var listeners = cbs.remove.length + 1;  ...  // recursively invoke hooks on child component root node  if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {   removeAndInvokeRemoveHook(i, rm);  }  for (i = 0; i < cbs.remove.length; ++i) {   cbs.remove[i](vnode, rm);  }  if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {   i(vnode, rm);  } else {   // 刪除id為app的老節點   rm();  } } else {  removeNode(vnode.elm); }}初次渲染結束。
更新節點過程
為了更好地測試,模板選用
<div id="app">{{ message }}<button @click="update">更新</button></div>點擊按鈕,會更新message,重新渲染視圖,生成的VNode為
{ asyncFactory: undefined, asyncMeta: undefined, children: [VNode, VNode], componentInstance: undefined, componentOptions: undefined, context: Vue實例, data: {attrs: {id: "app"}}, elm: undefined, fnContext: undefined, fnOptions: undefined, fnScopeId: undefined, isAsyncPlaceholder: false, isCloned: false, isComment: false, isOnce: false, isRootInsert: true, isStatic: false, key: undefined, ns: undefined, parent: undefined, raw: false, tag: "div", text: undefined, child: undefined}在組件更新的時候,preVnode和vnode都是存在的,執行
vm.$el = vm.__patch__(prevVnode, vnode);
實際上是運行以下函數
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
該函數首先判斷oldVnode和vnode是否相等,相等則立即返回
if (oldVnode === vnode) { return}如果兩者均為靜態節點且key值相等,且vnode是被克隆或者具有isOnce屬性時,vnode的組件實例componentInstance直接賦值
if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { vnode.componentInstance = oldVnode.componentInstance; return}接著對兩者的屬性值作對比,并更新
var oldCh = oldVnode.children;var ch = vnode.children;if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) {  // 以vnode為準更新oldVnode的不同屬性  cbs.update[i](oldVnode, vnode);  } if (isDef(i = data.hook) && isDef(i = i.update)) {   i(oldVnode, vnode);  }}vnode和oldVnode的對比以及相應的DOM操作具體如下:
// vnode不存在text屬性的情況if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 子節點不相等時,更新 if (oldCh !== ch) {   updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); } } else if (isDef(ch)) {  {  checkDuplicateKeys(ch);  }  // 只存在vnode的子節點,如果oldVnode存在text屬性,則將元素的文本內容清空,并新增elm節點  if (isDef(oldVnode.text)) {    nodeOps.setTextContent(elm, '');   }  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) {  // 如果只存在oldVnode的子節點,則刪除DOM的子節點  removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) {  // 只存在oldVnode有text屬性,將元素的文本清空  nodeOps.setTextContent(elm, ''); }} else if (oldVnode.text !== vnode.text) { // node和oldVnode的text屬性都存在且不一致時,元素節點內容設置為vnode.text nodeOps.setTextContent(elm, vnode.text);}對于子節點的對比,先分別定義oldVnode和vnode兩數組的前后兩個指針索引
var oldStartIdx = 0;var newStartIdx = 0;var oldEndIdx = oldCh.length - 1;var oldStartVnode = oldCh[0];var oldEndVnode = oldCh[oldEndIdx];var newEndIdx = newCh.length - 1;var newStartVnode = newCh[0];var newEndVnode = newCh[newEndIdx];var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
如下圖:

接下來是一個while循環,在這過程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 會逐漸向中間靠攏
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
當oldStartVnode或者oldEndVnode為空時,兩中間移動
if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left} else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx];} 接下來這一塊,是將 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 兩兩比對的過程,共四種:
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];}第一種: 前前相等比較

如果相等,則oldStartVnode.elm和newStartVnode.elm均向后移一位,繼續比較。 第二種: 后后相等比較

如果相等,則oldEndVnode.elm和newEndVnode.elm均向前移一位,繼續比較。 第三種: 前后相等比較

將oldStartVnode.elm節點直接移動到oldEndVnode.elm節點后面,然后將oldStartIdx向后移一位,newEndIdx向前移動一位。 第四種: 后前相等比較

將oldEndVnode.elm節點直接移動到oldStartVnode.elm節點后面,然后將oldEndIdx向前移一位,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];}createkeyToOldIdx函數的作用是建立key和index索引對應的map表,如果還是沒有找到節點,則新創建節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
插入到oldStartVnode.elm節點前面,否則,如果找到了節點,并符合sameVnode,將兩個節點patchVnode,并將該位置的老節點置為undefined,同時將vnodeToMove.elm移到oldStartVnode.elm的前面,以及newStartIdx往后移一位,示意圖如下:
 
如果不符合sameVnode,只能創建一個新節點插入到 parentElm 的子節點中,newStartIdx 往后移動一位。 最后如果,oldStartIdx > oldEndIdx,說明老節點比對完了,但是新節點還有多的,需要將新節點插入到真實 DOM 中去,調用 addVnodes 將這些節點插入即可;如果滿足 newStartIdx > newEndIdx 條件,說明新節點比對完了,老節點還有多,將這些無用的老節點通過 removeVnodes 批量刪除即可。到這里這個過程基本結束。
總結
以上所述是小編給大家介紹的Vue內部渲染視圖的方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對武林網網站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!
新聞熱點
疑難解答