6. Vue內部是如何渲染視圖

虛擬DOM與VNode簡介

  1. 什麼是虛擬DOM
  • 之前M的命令式操做DOM即便用jQuery操做DOM節點,隨着狀態的增多,DOM的操做就會愈來愈頻繁,程序的狀態也越難維護,如今主流的框架都是採用聲明式操做DOM,將操做DOM的方法封裝起來,咱們只要更改數據的狀態,框架自己會幫咱們操做DOM。
  • 虛擬DOM根據狀態創建一顆虛擬節點樹,新的虛擬節點樹會與舊的虛擬節點樹進行對比,只渲染髮生改變的部分,以下圖:
  1. 引入虛擬DOM的目的
  • 把渲染過程抽象化,從而使得組件的抽象能力也獲得提高,而且能夠適配DOM之外的渲染目標;
  • 能夠更好地支持SSR、同構渲染等;
  • 再也不依賴HTML解析器進行模板解析,能夠進行更多的AOT(預編譯)工做提升運行時效率,還能將Vue運行時體積進一步壓縮。
  1. 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有註釋節點、文本節點、元素節點、組件節點、函數式組件、克隆節點:vue

  • 註釋節點
var createEmptyVNode = function (text) {
    if ( text === void 0 ) text = '';
    var node = new VNode();
    node.text = text;
    node.isComment = true;
    return node
  };
複製代碼

只有isComment和text屬性有效,其他的默認爲false或者nullnode

  • 文本節點
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的實例 對應的vnodebash

new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    )
複製代碼

app

{
    componentOptions: {},
    componentInstance: {},
    tag: 'vue-component-1-child',
    data: {...},
    ...
}
複製代碼
  • 函數式組件 函數組件經過createFunctionalComponent函數建立, 跟組件節點相似,暫時沒看到特殊屬性,有的話後續再補上。

patch

虛擬DOM最重要的功能是patch,將VNode渲染爲真實的DOM。框架

patch簡介

patch中文意思是打補丁,也就是在原有的基礎上修改DOM節點,也能夠說是渲染視圖。DOM節點的修改有三種:async

  • 建立新增節點
  • 刪除廢棄的節點
  • 修改須要更新的節點。 當緩存上一次的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 element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);

// create new node
createElm(
    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 批量刪除便可。到這裏這個過程基本結束。

總結

本文詳細介紹了虛擬DOM的整個patch過程,如何到渲染到頁面,以及元素從視圖中刪除,最後是子節點的更新過程,包括了建立新增的子節點、刪除廢棄子節點、更新發生變化的子節點以及位置發生變化的子節點更新等。

參考文獻

剖析 Vue.js 內部運行機制

相關文章
相關標籤/搜索